--- a/net/minecraft/world/entity/npc/Villager.java
+++ b/net/minecraft/world/entity/npc/Villager.java
@@ -90,6 +_,14 @@
 import net.minecraft.world.phys.AABB;
 import org.slf4j.Logger;
 
+// CraftBukkit start
+import org.bukkit.Bukkit;
+import org.bukkit.craftbukkit.event.CraftEventFactory;
+import org.bukkit.event.entity.EntityRemoveEvent;
+import org.bukkit.event.entity.EntityTransformEvent;
+import org.bukkit.event.entity.VillagerReplenishTradeEvent;
+// CraftBukkit end
+
 public class Villager extends AbstractVillager implements ReputationEventHandler, VillagerDataHolder {
     private static final Logger LOGGER = LogUtils.getLogger();
     private static final EntityDataAccessor<VillagerData> DATA_VILLAGER_DATA = SynchedEntityData.defineId(Villager.class, EntityDataSerializers.VILLAGER_DATA);
@@ -115,7 +_,7 @@
     private long lastGossipDecayTime;
     private int villagerXp;
     private long lastRestockGameTime;
-    private int numberOfRestocksToday;
+    public int numberOfRestocksToday;
     private long lastRestockCheckDayTime;
     private boolean assignProfessionWhenSpawned;
     private static final ImmutableList<MemoryModuleType<?>> MEMORY_TYPES = ImmutableList.of(
@@ -257,11 +_,35 @@
         return this.assignProfessionWhenSpawned;
     }
 
+    // Paper start - EAR 2
+    @Override
+    public void inactiveTick() {
+        // SPIGOT-3874, SPIGOT-3894, SPIGOT-3846, SPIGOT-5286 :(
+        if (this.getUnhappyCounter() > 0) {
+            this.setUnhappyCounter(this.getUnhappyCounter() - 1);
+        }
+        if (this.isEffectiveAi()) {
+            if (this.level().spigotConfig.tickInactiveVillagers) {
+                this.customServerAiStep(this.level().getMinecraftWorld());
+            } else {
+                this.customServerAiStep(this.level().getMinecraftWorld(), true);
+            }
+        }
+        maybeDecayGossip();
+        super.inactiveTick();
+    }
+    // Paper end - EAR 2
+
     @Override
     protected void customServerAiStep(ServerLevel level) {
+        // Paper start - EAR 2
+        this.customServerAiStep(level, false);
+    }
+    protected void customServerAiStep(ServerLevel level, final boolean inactive) {
+        // Paper end - EAR 2
         ProfilerFiller profilerFiller = Profiler.get();
         profilerFiller.push("villagerBrain");
-        this.getBrain().tick(level, this);
+        if (!inactive) this.getBrain().tick(level, this); // Paper - EAR 2
         profilerFiller.pop();
         if (this.assignProfessionWhenSpawned) {
             this.assignProfessionWhenSpawned = false;
@@ -275,7 +_,7 @@
                     this.increaseProfessionLevelOnUpdate = false;
                 }
 
-                this.addEffect(new MobEffectInstance(MobEffects.REGENERATION, 200, 0));
+                this.addEffect(new MobEffectInstance(MobEffects.REGENERATION, 200, 0), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.VILLAGER_TRADE); // CraftBukkit
             }
         }
 
@@ -285,7 +_,7 @@
             this.lastTradedPlayer = null;
         }
 
-        if (!this.isNoAi() && this.random.nextInt(100) == 0) {
+        if (!inactive && !this.isNoAi() && this.random.nextInt(100) == 0) { // Paper - EAR 2
             Raid raidAt = level.getRaidAt(this.blockPosition());
             if (raidAt != null && raidAt.isActive() && !raidAt.isOver()) {
                 level.broadcastEntityEvent(this, (byte)42);
@@ -295,6 +_,7 @@
         if (this.getVillagerData().getProfession() == VillagerProfession.NONE && this.isTrading()) {
             this.stopTrading();
         }
+        if (inactive) return; // Paper - EAR 2
 
         super.customServerAiStep(level);
     }
@@ -339,7 +_,7 @@
         }
     }
 
-    private void setUnhappy() {
+    public void setUnhappy() {
         this.setUnhappyCounter(40);
         if (!this.level().isClientSide()) {
             this.makeSound(SoundEvents.VILLAGER_NO);
@@ -384,7 +_,13 @@
         this.updateDemand();
 
         for (MerchantOffer merchantOffer : this.getOffers()) {
-            merchantOffer.resetUses();
+            // CraftBukkit start
+            VillagerReplenishTradeEvent event = new VillagerReplenishTradeEvent((org.bukkit.entity.Villager) this.getBukkitEntity(), merchantOffer.asBukkit());
+            Bukkit.getPluginManager().callEvent(event);
+            if (!event.isCancelled()) {
+                merchantOffer.resetUses();
+            }
+            // CraftBukkit end
         }
 
         this.resendOffersToTradingPlayer();
@@ -445,7 +_,13 @@
         int i = 2 - this.numberOfRestocksToday;
         if (i > 0) {
             for (MerchantOffer merchantOffer : this.getOffers()) {
-                merchantOffer.resetUses();
+                // CraftBukkit start
+                VillagerReplenishTradeEvent event = new VillagerReplenishTradeEvent((org.bukkit.entity.Villager) this.getBukkitEntity(), merchantOffer.asBukkit());
+                Bukkit.getPluginManager().callEvent(event);
+                if (!event.isCancelled()) {
+                    merchantOffer.resetUses();
+                }
+                // CraftBukkit end
             }
         }
 
@@ -466,6 +_,7 @@
         int playerReputation = this.getPlayerReputation(player);
         if (playerReputation != 0) {
             for (MerchantOffer merchantOffer : this.getOffers()) {
+                if (merchantOffer.ignoreDiscounts) continue; // Paper - Add ignore discounts API
                 merchantOffer.addToSpecialPriceDiff(-Mth.floor(playerReputation * merchantOffer.getPriceMultiplier()));
             }
         }
@@ -475,6 +_,7 @@
             int amplifier = effect.getAmplifier();
 
             for (MerchantOffer merchantOffer1 : this.getOffers()) {
+                if (merchantOffer1.ignoreDiscounts) continue; // Paper - Add ignore discounts API
                 double d = 0.3 + 0.0625 * amplifier;
                 int i = (int)Math.floor(d * merchantOffer1.getBaseCostA().getCount());
                 merchantOffer1.addToSpecialPriceDiff(-Math.max(i, 1));
@@ -559,7 +_,7 @@
     }
 
     @Override
-    protected SoundEvent getDeathSound() {
+    public SoundEvent getDeathSound() {
         return SoundEvents.VILLAGER_DEATH;
     }
 
@@ -594,7 +_,7 @@
         }
 
         if (offer.shouldRewardExp()) {
-            this.level().addFreshEntity(new ExperienceOrb(this.level(), this.getX(), this.getY() + 0.5, this.getZ(), i));
+            this.level().addFreshEntity(new ExperienceOrb(this.level(), this.getX(), this.getY() + 0.5, this.getZ(), i, org.bukkit.entity.ExperienceOrb.SpawnReason.VILLAGER_TRADE, this.getTradingPlayer(), this)); // Paper
         }
     }
 
@@ -612,7 +_,7 @@
 
     @Override
     public void die(DamageSource cause) {
-        LOGGER.info("Villager {} died, message: '{}'", this, cause.getLocalizedDeathMessage(this).getString());
+        if (org.spigotmc.SpigotConfig.logVillagerDeaths) LOGGER.info("Villager {} died, message: '{}'", this, cause.getLocalizedDeathMessage(this).getString()); // Spigot
         Entity entity = cause.getEntity();
         if (entity != null) {
             this.tellWitnessesThatIWasMurdered(entity);
@@ -622,7 +_,7 @@
         super.die(cause);
     }
 
-    private void releaseAllPois() {
+    public void releaseAllPois() {
         this.releasePoi(MemoryModuleType.HOME);
         this.releasePoi(MemoryModuleType.JOB_SITE);
         this.releasePoi(MemoryModuleType.POTENTIAL_JOB_SITE);
@@ -711,7 +_,7 @@
         return VillagerData.canLevelUp(level) && this.villagerXp >= VillagerData.getMaxXpPerLevel(level);
     }
 
-    private void increaseMerchantCareer() {
+    public void increaseMerchantCareer() {
         this.setVillagerData(this.getVillagerData().setLevel(this.getVillagerData().getLevel() + 1));
         this.updateTrades();
     }
@@ -782,12 +_,19 @@
     @Override
     public void thunderHit(ServerLevel level, LightningBolt lightning) {
         if (level.getDifficulty() != Difficulty.PEACEFUL) {
-            LOGGER.info("Villager {} was struck by lightning {}.", this, lightning);
+            // Paper - Add EntityZapEvent; move log down, event can cancel
             Witch witch = this.convertTo(EntityType.WITCH, ConversionParams.single(this, false, false), mob -> {
+                // Paper start - Add EntityZapEvent
+                if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityZapEvent(this, lightning, mob).isCancelled()) {
+                    return false;
+                }
+                if (org.spigotmc.SpigotConfig.logVillagerDeaths) Villager.LOGGER.info("Villager {} was struck by lightning {}.", this, lightning); // Move down
+                // Paper end - Add EntityZapEvent
                 mob.finalizeSpawn(level, level.getCurrentDifficultyAt(mob.blockPosition()), EntitySpawnReason.CONVERSION, null);
                 mob.setPersistenceRequired();
                 this.releaseAllPois();
-            });
+                return true; // Paper start - Add EntityZapEvent
+            }, EntityTransformEvent.TransformReason.LIGHTNING, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.LIGHTNING); // CraftBukkit
             if (witch == null) {
                 super.thunderHit(level, lightning);
             }
@@ -827,6 +_,12 @@
 
     @Override
     protected void updateTrades() {
+        // Paper start - More vanilla friendly methods to update trades
+        updateTrades(TRADES_PER_LEVEL);
+    }
+
+    public boolean updateTrades(int amount) {
+        // Paper end - More vanilla friendly methods to update trades
         VillagerData villagerData = this.getVillagerData();
         Int2ObjectMap<VillagerTrades.ItemListing[]> map1;
         if (this.level().enabledFeatures().contains(FeatureFlags.TRADE_REBALANCE)) {
@@ -840,9 +_,11 @@
             VillagerTrades.ItemListing[] itemListings = map1.get(villagerData.getLevel());
             if (itemListings != null) {
                 MerchantOffers offers = this.getOffers();
-                this.addOffersFromItemListings(offers, itemListings, 2);
+                this.addOffersFromItemListings(offers, itemListings, amount); // Paper - More vanilla friendly methods to update trades
+                return true; // Paper - More vanilla friendly methods to update trades
             }
         }
+        return false; // Paper - More vanilla friendly methods to update trades
     }
 
     public void gossip(ServerLevel serverLevel, Villager target, long gameTime) {
@@ -871,7 +_,7 @@
             List<Villager> entitiesOfClass = serverLevel.getEntitiesOfClass(Villager.class, aabb);
             List<Villager> list = entitiesOfClass.stream().filter(villager -> villager.wantsToSpawnGolem(gameTime)).limit(5L).toList();
             if (list.size() >= minVillagerAmount) {
-                if (!SpawnUtil.trySpawnMob(
+                if (SpawnUtil.trySpawnMob( // Paper - Set Golem Last Seen to stop it from spawning another one - switch to isPresent
                         EntityType.IRON_GOLEM,
                         EntitySpawnReason.MOB_SUMMONED,
                         serverLevel,
@@ -880,9 +_,11 @@
                         8,
                         6,
                         SpawnUtil.Strategy.LEGACY_IRON_GOLEM,
-                        false
+                        false,
+                        org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.VILLAGE_DEFENSE, // CraftBukkit,
+                        () -> {GolemSensor.golemDetected(this);} // Paper - Set Golem Last Seen to stop it from spawning another one
                     )
-                    .isEmpty()) {
+                    .isPresent()) { // Paper - Set Golem Last Seen to stop it from spawning another one - switch to isPresent
                     entitiesOfClass.forEach(GolemSensor::golemDetected);
                 }
             }
