--- a/net/minecraft/world/level/block/entity/trialspawner/TrialSpawner.java
+++ b/net/minecraft/world/level/block/entity/trialspawner/TrialSpawner.java
@@ -58,12 +_,12 @@
     private static final int MAX_MOB_TRACKING_DISTANCE_SQR = Mth.square(47);
     private static final float SPAWNING_AMBIENT_SOUND_CHANCE = 0.02F;
     private final TrialSpawnerStateData data = new TrialSpawnerStateData();
-    private TrialSpawner.FullConfig config;
-    private final TrialSpawner.StateAccessor stateAccessor;
+    public TrialSpawner.FullConfig config;
+    public final TrialSpawner.StateAccessor stateAccessor;
     private PlayerDetector playerDetector;
     private final PlayerDetector.EntitySelector entitySelector;
     private boolean overridePeacefulAndMobSpawnRule;
-    private boolean isOminous;
+    public boolean isOminous;
 
     public TrialSpawner(
         TrialSpawner.FullConfig config, TrialSpawner.StateAccessor stateAccessor, PlayerDetector playerDetector, PlayerDetector.EntitySelector entitySelector
@@ -217,7 +_,14 @@
                 nextSpawnData.getEquipment().ifPresent(mob::equip);
             }
 
-            if (!level.tryAddFreshEntityWithPassengers(entity)) {
+            // Paper start - TrialSpawnerSpawnEvent + SpawnReason
+            entity.spawnedViaMobSpawner = true; // Mark entity as spawned via spawner
+            entity.spawnReason = org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.TRIAL_SPAWNER; // Paper - Entity#getEntitySpawnReason
+            if (org.bukkit.craftbukkit.event.CraftEventFactory.callTrialSpawnerSpawnEvent(entity, pos).isCancelled()) {
+                return Optional.empty();
+            }
+            if (!level.tryAddFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.TRIAL_SPAWNER)) {
+                // Paper end - TrialSpawnerSpawnEvent + SpawnReason
                 return Optional.empty();
             }
 
@@ -232,10 +_,24 @@
     }
 
     public void ejectReward(ServerLevel level, BlockPos pos, ResourceKey<LootTable> lootTable) {
-        LootTable lootTable1 = level.getServer().reloadableRegistries().getLootTable(lootTable);
+        LootTable lootTable1 = level.getServer().reloadableRegistries().getLootTable(lootTable); final LootTable sourceLootTable = lootTable1; // Paper - obfhelper
         LootParams lootParams = new LootParams.Builder(level).create(LootContextParamSets.EMPTY);
         ObjectArrayList<ItemStack> randomItems = lootTable1.getRandomItems(lootParams);
         if (!randomItems.isEmpty()) {
+            // CraftBukkit start
+            org.bukkit.event.block.BlockDispenseLootEvent spawnerDispenseLootEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callBlockDispenseLootEvent(
+                level,
+                pos,
+                null,
+                randomItems,
+                sourceLootTable
+            );
+            if (spawnerDispenseLootEvent.isCancelled()) {
+                return;
+            }
+
+            randomItems = new ObjectArrayList<>(spawnerDispenseLootEvent.getDispensedLoot().stream().map(org.bukkit.craftbukkit.inventory.CraftItemStack::asNMSCopy).toList());
+            // CraftBukkit end
             for (ItemStack itemStack : randomItems) {
                 DefaultDispenseItemBehavior.spawnItem(level, itemStack, 2, Direction.UP, Vec3.atBottomCenterOf(pos).relative(Direction.UP, 1.2));
             }
@@ -405,6 +_,35 @@
                 this.requiredPlayerRange
             );
         }
+
+        // Paper start - trial spawner API - withers
+        public TrialSpawner.FullConfig overrideTargetCooldownLength(final int targetCooldownLength) {
+            return new TrialSpawner.FullConfig(
+                this.normal,
+                this.ominous,
+                targetCooldownLength,
+                this.requiredPlayerRange
+            );
+        }
+
+        public TrialSpawner.FullConfig overrideRequiredPlayerRange(final int requiredPlayerRange) {
+            return new TrialSpawner.FullConfig(
+                this.normal,
+                this.ominous,
+                this.targetCooldownLength,
+                requiredPlayerRange
+            );
+        }
+
+        public TrialSpawner.FullConfig overrideConfigs(final Holder<TrialSpawnerConfig> normal, final Holder<TrialSpawnerConfig> ominous) {
+            return new TrialSpawner.FullConfig(
+                normal,
+                ominous,
+                this.targetCooldownLength,
+                this.requiredPlayerRange
+            );
+        }
+        // Paper end - trial spawner API - withers
     }
 
     public interface StateAccessor {
