--- a/net/minecraft/world/entity/npc/AbstractVillager.java
+++ b/net/minecraft/world/entity/npc/AbstractVillager.java
@@ -34,8 +34,23 @@
 import net.minecraft.world.level.ServerLevelAccessor;
 import net.minecraft.world.level.pathfinder.BlockPathTypes;
 import net.minecraft.world.phys.Vec3;
+// CraftBukkit start
+import org.bukkit.Bukkit;
+import org.bukkit.craftbukkit.v1_20_R2.inventory.CraftMerchant;
+import org.bukkit.craftbukkit.v1_20_R2.inventory.CraftMerchantRecipe;
+import org.bukkit.event.entity.VillagerAcquireTradeEvent;
+// CraftBukkit end
 
 public abstract class AbstractVillager extends AgeableMob implements InventoryCarrier, Npc, Merchant {
+
+    // CraftBukkit start
+    private CraftMerchant craftMerchant;
+
+    @Override
+    public CraftMerchant getCraftMerchant() {
+        return (this.craftMerchant == null) ? this.craftMerchant = new CraftMerchant(this) : this.craftMerchant;
+    }
+    // CraftBukkit end
     private static final EntityDataAccessor<Integer> DATA_UNHAPPY_COUNTER = SynchedEntityData.defineId(AbstractVillager.class, EntityDataSerializers.INT);
     public static final int VILLAGER_SLOT_OFFSET = 300;
     private static final int VILLAGER_INVENTORY_SIZE = 8;
@@ -43,7 +58,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.v1_20_R2.entity.CraftAbstractVillager) this.getBukkitEntity()); // CraftBukkit add argument
 
     public AbstractVillager(EntityType<? extends AbstractVillager> type, Level world) {
         super(type, world);
@@ -57,15 +72,15 @@
             entityData = new AgeableMob.AgeableMobGroupData(false);
         }
 
-        return super.finalizeSpawn(world, difficulty, spawnReason, entityData, entityNbt);
+        return super.finalizeSpawn(world, difficulty, spawnReason, (SpawnGroupData) entityData, entityNbt);
     }
 
     public int getUnhappyCounter() {
-        return this.entityData.get(DATA_UNHAPPY_COUNTER);
+        return (Integer) this.entityData.get(AbstractVillager.DATA_UNHAPPY_COUNTER);
     }
 
     public void setUnhappyCounter(int ticks) {
-        this.entityData.set(DATA_UNHAPPY_COUNTER, ticks);
+        this.entityData.set(AbstractVillager.DATA_UNHAPPY_COUNTER, ticks);
     }
 
     @Override
@@ -81,7 +96,7 @@
     @Override
     protected void defineSynchedData() {
         super.defineSynchedData();
-        this.entityData.define(DATA_UNHAPPY_COUNTER, 0);
+        this.entityData.define(AbstractVillager.DATA_UNHAPPY_COUNTER, 0);
     }
 
     @Override
@@ -99,6 +114,13 @@
         return this.tradingPlayer != null;
     }
 
+    // Paper start
+    public void resetOffers() {
+        this.offers = new MerchantOffers();
+        this.updateTrades();
+    }
+    // Paper end
+
     @Override
     public MerchantOffers getOffers() {
         if (this.offers == null) {
@@ -110,20 +132,31 @@
     }
 
     @Override
-    public void overrideOffers(@Nullable MerchantOffers offers) {
-    }
+    public void overrideOffers(@Nullable MerchantOffers offers) {}
 
     @Override
-    public void overrideXp(int experience) {
+    public void overrideXp(int experience) {}
+
+    // Paper start
+    @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
 
     @Override
     public void notifyTrade(MerchantOffer offer) {
-        offer.increaseUses();
+        // offer.increaseUses(); // Paper - handled in processTrade
         this.ambientSoundTime = -this.getAmbientSoundInterval();
-        this.rewardTradeXp(offer);
+        // this.rewardTradeXp(offer); // Paper - handled in processTrade
         if (this.tradingPlayer instanceof ServerPlayer) {
-            CriteriaTriggers.TRADE.trigger((ServerPlayer)this.tradingPlayer, this, offer.getResult());
+            CriteriaTriggers.TRADE.trigger((ServerPlayer) this.tradingPlayer, this, offer.getResult());
         }
 
     }
@@ -160,9 +193,10 @@
     @Override
     public void addAdditionalSaveData(CompoundTag nbt) {
         super.addAdditionalSaveData(nbt);
-        MerchantOffers merchantOffers = this.getOffers();
-        if (!merchantOffers.isEmpty()) {
-            nbt.put("Offers", merchantOffers.createTag());
+        MerchantOffers merchantrecipelist = this.getOffers();
+
+        if (!merchantrecipelist.isEmpty()) {
+            nbt.put("Offers", merchantrecipelist.createTag());
         }
 
         this.writeInventoryToTag(nbt);
@@ -186,7 +220,7 @@
     }
 
     protected void stopTrading() {
-        this.setTradingPlayer((Player)null);
+        this.setTradingPlayer((Player) null);
     }
 
     @Override
@@ -196,11 +230,12 @@
     }
 
     protected void addParticlesAroundSelf(ParticleOptions parameters) {
-        for(int i = 0; i < 5; ++i) {
-            double d = this.random.nextGaussian() * 0.02D;
-            double e = this.random.nextGaussian() * 0.02D;
-            double f = this.random.nextGaussian() * 0.02D;
-            this.level().addParticle(parameters, this.getRandomX(1.0D), this.getRandomY() + 1.0D, this.getRandomZ(1.0D), d, e, f);
+        for (int i = 0; i < 5; ++i) {
+            double d0 = this.random.nextGaussian() * 0.02D;
+            double d1 = this.random.nextGaussian() * 0.02D;
+            double d2 = this.random.nextGaussian() * 0.02D;
+
+            this.level().addParticle(parameters, this.getRandomX(1.0D), this.getRandomY() + 1.0D, this.getRandomZ(1.0D), d0, d1, d2);
         }
 
     }
@@ -217,21 +252,36 @@
 
     @Override
     public SlotAccess getSlot(int mappedIndex) {
-        int i = mappedIndex - 300;
-        return i >= 0 && i < this.inventory.getContainerSize() ? SlotAccess.forContainer(this.inventory, i) : super.getSlot(mappedIndex);
+        int j = mappedIndex - 300;
+
+        return j >= 0 && j < this.inventory.getContainerSize() ? SlotAccess.forContainer(this.inventory, j) : super.getSlot(mappedIndex);
     }
 
     protected abstract void updateTrades();
 
     protected void addOffersFromItemListings(MerchantOffers recipeList, VillagerTrades.ItemListing[] pool, int count) {
-        ArrayList<VillagerTrades.ItemListing> arrayList = Lists.newArrayList(pool);
-        int i = 0;
+        ArrayList<VillagerTrades.ItemListing> arraylist = Lists.newArrayList(pool);
+        int j = 0;
 
-        while(i < count && !arrayList.isEmpty()) {
-            MerchantOffer merchantOffer = arrayList.remove(this.random.nextInt(arrayList.size())).getOffer(this, this.random);
-            if (merchantOffer != null) {
-                recipeList.add(merchantOffer);
-                ++i;
+        while (j < count && !arraylist.isEmpty()) {
+            MerchantOffer merchantrecipe = ((VillagerTrades.ItemListing) arraylist.remove(this.random.nextInt(arraylist.size()))).getOffer(this, this.random);
+
+            if (merchantrecipe != null) {
+                // CraftBukkit start
+                VillagerAcquireTradeEvent event = new VillagerAcquireTradeEvent((org.bukkit.entity.AbstractVillager) this.getBukkitEntity(), merchantrecipe.asBukkit());
+                // Suppress during worldgen
+                if (this.valid) {
+                    Bukkit.getPluginManager().callEvent(event);
+                }
+                if (!event.isCancelled()) {
+                    // Paper start
+                    final CraftMerchantRecipe craftMerchantRecipe = CraftMerchantRecipe.fromBukkit(event.getRecipe());
+                    if (craftMerchantRecipe.getIngredients().isEmpty()) return;
+                    recipeList.add(craftMerchantRecipe.toMinecraft());
+                    // Paper end
+                }
+                // CraftBukkit end
+                ++j;
             }
         }
 
@@ -239,9 +289,10 @@
 
     @Override
     public Vec3 getRopeHoldPosition(float delta) {
-        float f = Mth.lerp(delta, this.yBodyRotO, this.yBodyRot) * ((float)Math.PI / 180F);
-        Vec3 vec3 = new Vec3(0.0D, this.getBoundingBox().getYsize() - 1.0D, 0.2D);
-        return this.getPosition(delta).add(vec3.yRot(-f));
+        float f1 = Mth.lerp(delta, this.yBodyRotO, this.yBodyRot) * 0.017453292F;
+        Vec3 vec3d = new Vec3(0.0D, this.getBoundingBox().getYsize() - 1.0D, 0.2D);
+
+        return this.getPosition(delta).add(vec3d.yRot(-f1));
     }
 
     @Override
