aboutsummaryrefslogtreecommitdiffhomepage
path: root/patches/server/0674-More-Projectile-API.patch
blob: a6794c62b0ffe2a3284a1b8131d778779a6a2e63 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com>
Date: Tue, 22 Jun 2021 23:41:11 -0400
Subject: [PATCH] More Projectile API

== AT ==
public net.minecraft.world.entity.projectile.FishingHook timeUntilLured
public net.minecraft.world.entity.projectile.FishingHook fishAngle
public net.minecraft.world.entity.projectile.ShulkerBullet targetDeltaX
public net.minecraft.world.entity.projectile.ShulkerBullet targetDeltaY
public net.minecraft.world.entity.projectile.ShulkerBullet targetDeltaZ
public net.minecraft.world.entity.projectile.ShulkerBullet currentMoveDirection
public net.minecraft.world.entity.projectile.ShulkerBullet flightSteps
public net.minecraft.world.entity.projectile.AbstractArrow soundEvent
public net/minecraft/world/entity/projectile/AbstractArrow setPickupItemStack(Lnet/minecraft/world/item/ItemStack;)V
public net.minecraft.world.entity.projectile.ThrownTrident dealtDamage
public net.minecraft.world.entity.projectile.Arrow NO_EFFECT_COLOR
public net.minecraft.world.entity.projectile.Projectile hasBeenShot
public net.minecraft.world.entity.projectile.Projectile leftOwner
public net.minecraft.world.entity.projectile.Projectile preOnHit(Lnet/minecraft/world/phys/HitResult;)V
public net.minecraft.world.entity.projectile.Projectile canHitEntity(Lnet/minecraft/world/entity/Entity;)Z
public net.minecraft.world.entity.projectile.FireworkRocketEntity getDefaultItem()Lnet/minecraft/world/item/ItemStack;
public net.minecraft.world.item.CrossbowItem FIREWORK_POWER

Co-authored-by: Nassim Jahnke <nassim@njahnke.dev>
Co-authored-by: SoSeDiK <mrsosedik@gmail.com>
Co-authored-by: MelnCat <melncatuwu@gmail.com>
Co-authored-by: Lulu13022002 <41980282+Lulu13022002@users.noreply.github.com>

diff --git a/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java b/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java
index 5b7734020b496ade3740d92908ad2d399bfd55e6..e70ca1b2e6fbbc1f20e65429298d01b4ebd2dd29 100644
--- a/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java
+++ b/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java
@@ -414,13 +414,18 @@ public class FishingHook extends Projectile {
                 }
             } else {
                 // CraftBukkit start - logic to modify fishing wait time
-                this.timeUntilLured = Mth.nextInt(this.random, this.minWaitTime, this.maxWaitTime);
-                this.timeUntilLured -= (this.applyLure) ? (this.lureSpeed >= this.maxWaitTime ? this.timeUntilLured - 1 : this.lureSpeed ) : 0; // Paper - Fix Lure infinite loop
+                this.resetTimeUntilLured(); // Paper - more projectile api - extract time until lured reset logic
                 // CraftBukkit end
             }
         }
 
     }
+    // Paper start - more projectile api - extract time until lured reset logic
+    public void resetTimeUntilLured() {
+        this.timeUntilLured = Mth.nextInt(this.random, this.minWaitTime, this.maxWaitTime);
+        this.timeUntilLured -= (this.applyLure) ? (this.lureSpeed >= this.maxWaitTime ? this.timeUntilLured - 1 : this.lureSpeed ) : 0; // Paper - Fix Lure infinite loop
+    }
+    // Paper end - more projectile api - extract time until lured reset logic
 
     public boolean calculateOpenWater(BlockPos pos) {
         FishingHook.OpenWaterType entityfishinghook_waterposition = FishingHook.OpenWaterType.INVALID;
diff --git a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java
index d27e17ebf25cd842a943cf82bde05b2248c74414..06ca07edef062f21c51860146086297ca345104d 100644
--- a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java
+++ b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java
@@ -190,7 +190,7 @@ public abstract class Projectile extends Entity implements TraceableEntity {
     }
 
     // CraftBukkit start - call projectile hit event
-    protected ProjectileDeflection preHitTargetOrDeflectSelf(HitResult movingobjectposition) {
+    public ProjectileDeflection preHitTargetOrDeflectSelf(HitResult movingobjectposition) { // Paper - protected -> public
         org.bukkit.event.entity.ProjectileHitEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callProjectileHitEvent(this, movingobjectposition);
         this.hitCancelled = event != null && event.isCancelled();
         if (movingobjectposition.getType() == HitResult.Type.BLOCK || !this.hitCancelled) {
diff --git a/src/main/java/net/minecraft/world/entity/projectile/ThrownPotion.java b/src/main/java/net/minecraft/world/entity/projectile/ThrownPotion.java
index cb34cc9443da56c0497c7a0192c8b8363c3426fe..58dc69fe319027c2b9ecfb9caf272368e81081df 100644
--- a/src/main/java/net/minecraft/world/entity/projectile/ThrownPotion.java
+++ b/src/main/java/net/minecraft/world/entity/projectile/ThrownPotion.java
@@ -102,6 +102,11 @@ public class ThrownPotion extends ThrowableItemProjectile implements ItemSupplie
     @Override
     protected void onHit(HitResult hitResult) {
         super.onHit(hitResult);
+        // Paper start - More projectile API
+        this.splash(hitResult);
+    }
+    public void splash(@Nullable HitResult hitResult) {
+        // Paper end - More projectile API
         if (!this.level().isClientSide) {
             ItemStack itemstack = this.getItem();
             PotionContents potioncontents = (PotionContents) itemstack.getOrDefault(DataComponents.POTION_CONTENTS, PotionContents.EMPTY);
@@ -113,7 +118,7 @@ public class ThrownPotion extends ThrowableItemProjectile implements ItemSupplie
                 if (this.isLingering()) {
                     showParticles = this.makeAreaOfEffectCloud(potioncontents, hitResult); // CraftBukkit - Pass MovingObjectPosition // Paper
                 } else {
-                    showParticles = this.applySplash(potioncontents.getAllEffects(), hitResult.getType() == HitResult.Type.ENTITY ? ((EntityHitResult) hitResult).getEntity() : null, hitResult); // CraftBukkit - Pass MovingObjectPosition // Paper
+                    showParticles = this.applySplash(potioncontents.getAllEffects(), hitResult != null && hitResult.getType() == HitResult.Type.ENTITY ? ((EntityHitResult) hitResult).getEntity() : null, hitResult); // CraftBukkit - Pass MovingObjectPosition // Paper - More projectile API
                 }
             }
 
@@ -175,7 +180,7 @@ public class ThrownPotion extends ThrowableItemProjectile implements ItemSupplie
 
     }
 
-    private boolean applySplash(Iterable<MobEffectInstance> iterable, @Nullable Entity entity, HitResult position) { // CraftBukkit - Pass MovingObjectPosition // Paper - Fix potions splash events
+    private boolean applySplash(Iterable<MobEffectInstance> iterable, @Nullable Entity entity, @Nullable HitResult position) { // CraftBukkit - Pass MovingObjectPosition // Paper - Fix potions splash events & More projectile API
         AABB axisalignedbb = this.getBoundingBox().inflate(4.0D, 2.0D, 4.0D);
         List<net.minecraft.world.entity.LivingEntity> list = this.level().getEntitiesOfClass(net.minecraft.world.entity.LivingEntity.class, axisalignedbb);
         Map<LivingEntity, Double> affected = new HashMap<LivingEntity, Double>(); // CraftBukkit
@@ -253,7 +258,7 @@ public class ThrownPotion extends ThrowableItemProjectile implements ItemSupplie
 
     }
 
-    private boolean makeAreaOfEffectCloud(PotionContents potioncontents, HitResult position) { // CraftBukkit - Pass MovingObjectPosition
+    private boolean makeAreaOfEffectCloud(PotionContents potioncontents, @Nullable HitResult position) { // CraftBukkit - Pass MovingObjectPosition // Paper - More projectile API
         AreaEffectCloud entityareaeffectcloud = new AreaEffectCloud(this.level(), this.getX(), this.getY(), this.getZ());
         Entity entity = this.getOwner();
 
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/AbstractProjectile.java b/src/main/java/org/bukkit/craftbukkit/entity/AbstractProjectile.java
index 91c2d0b40d3fca86938cd454e1415a4eea3df7c7..de4fb2654c7895cfd83ad694455ee56cb708c2f2 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/AbstractProjectile.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/AbstractProjectile.java
@@ -17,4 +17,65 @@ public abstract class AbstractProjectile extends CraftEntity implements Projecti
     @Override
     public void setBounce(boolean doesBounce) {}
 
+    // Paper start - More projectile API
+    @Override
+    public boolean hasLeftShooter() {
+        return this.getHandle().leftOwner;
+    }
+
+    @Override
+    public void setHasLeftShooter(boolean leftShooter) {
+        this.getHandle().leftOwner = leftShooter;
+    }
+
+    @Override
+    public boolean hasBeenShot() {
+        return this.getHandle().hasBeenShot;
+    }
+
+    @Override
+    public void setHasBeenShot(boolean beenShot) {
+        this.getHandle().hasBeenShot = beenShot;
+    }
+
+    @Override
+    public boolean canHitEntity(org.bukkit.entity.Entity entity) {
+        return this.getHandle().canHitEntity(((CraftEntity) entity).getHandle());
+    }
+
+    @Override
+    public void hitEntity(org.bukkit.entity.Entity entity) {
+        this.getHandle().preHitTargetOrDeflectSelf(new net.minecraft.world.phys.EntityHitResult(((CraftEntity) entity).getHandle()));
+    }
+
+    @Override
+    public void hitEntity(org.bukkit.entity.Entity entity, org.bukkit.util.Vector vector) {
+        this.getHandle().preHitTargetOrDeflectSelf(new net.minecraft.world.phys.EntityHitResult(((CraftEntity) entity).getHandle(), new net.minecraft.world.phys.Vec3(vector.getX(), vector.getY(), vector.getZ())));
+    }
+
+    @Override
+    public net.minecraft.world.entity.projectile.Projectile getHandle() {
+        return (net.minecraft.world.entity.projectile.Projectile) entity;
+    }
+
+    @Override
+    public final org.bukkit.projectiles.ProjectileSource getShooter() {
+        return this.getHandle().projectileSource;
+    }
+
+    @Override
+    public final void setShooter(org.bukkit.projectiles.ProjectileSource shooter) {
+        if (shooter instanceof CraftEntity craftEntity) {
+            this.getHandle().setOwner(craftEntity.getHandle());
+        } else {
+            this.getHandle().setOwner(null);
+        }
+        this.getHandle().projectileSource = shooter;
+    }
+
+    @Override
+    public java.util.UUID getOwnerUniqueId() {
+        return this.getHandle().ownerUUID;
+    }
+    // Paper end - More projectile API
 }
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractArrow.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractArrow.java
index 329ca9c743a7f2feeabbfb769ff9a71f60165006..faa08ad912fa43e7a6c5a2359e23c04c059c5edf 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractArrow.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractArrow.java
@@ -58,20 +58,7 @@ public class CraftAbstractArrow extends AbstractProjectile implements AbstractAr
         this.getHandle().setCritArrow(critical);
     }
 
-    @Override
-    public ProjectileSource getShooter() {
-        return this.getHandle().projectileSource;
-    }
-
-    @Override
-    public void setShooter(ProjectileSource shooter) {
-        if (shooter instanceof Entity) {
-            this.getHandle().setOwner(((CraftEntity) shooter).getHandle());
-        } else {
-            this.getHandle().setOwner(null);
-        }
-        this.getHandle().projectileSource = shooter;
-    }
+    // Paper - moved to AbstractProjectile
 
     @Override
     public boolean isInBlock() {
@@ -130,6 +117,7 @@ public class CraftAbstractArrow extends AbstractProjectile implements AbstractAr
 
     @Override
     public ItemStack getWeapon() {
+        if (this.getHandle().getWeaponItem() == null) return null; // Paper - fix NPE
         return CraftItemStack.asBukkitCopy(this.getHandle().getWeaponItem());
     }
 
@@ -149,4 +137,37 @@ public class CraftAbstractArrow extends AbstractProjectile implements AbstractAr
     public String toString() {
         return "CraftArrow";
     }
+
+    // Paper start
+    @Override
+    public CraftItemStack getItemStack() {
+        return CraftItemStack.asCraftMirror(this.getHandle().getPickupItem());
+    }
+
+    @Override
+    public void setItemStack(final ItemStack stack) {
+        Preconditions.checkArgument(stack != null, "ItemStack cannot be null");
+        this.getHandle().setPickupItemStack(CraftItemStack.asNMSCopy(stack));
+    }
+
+    @Override
+    public void setLifetimeTicks(int ticks) {
+        this.getHandle().life = ticks;
+    }
+
+    @Override
+    public int getLifetimeTicks() {
+        return this.getHandle().life;
+    }
+
+    @Override
+    public org.bukkit.Sound getHitSound() {
+        return org.bukkit.craftbukkit.CraftSound.minecraftToBukkit(this.getHandle().soundEvent);
+    }
+
+    @Override
+    public void setHitSound(org.bukkit.Sound sound) {
+        this.getHandle().setSoundEvent(org.bukkit.craftbukkit.CraftSound.bukkitToMinecraft(sound));
+    }
+    // Paper end
 }
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftAreaEffectCloud.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftAreaEffectCloud.java
index 81f5e1d866128af8fb2acc13aca715580fdf9886..88f2a9f310f30a08893f3fa68af13a54cf72fa7f 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftAreaEffectCloud.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftAreaEffectCloud.java
@@ -125,7 +125,7 @@ public class CraftAreaEffectCloud extends CraftEntity implements AreaEffectCloud
 
     @Override
     public Color getColor() {
-        return Color.fromRGB(this.getHandle().potionContents.getColor());
+        return Color.fromRGB(this.getHandle().potionContents.getColor() & 0x00FFFFFF); // Paper - skip alpha channel
     }
 
     @Override
@@ -143,7 +143,7 @@ public class CraftAreaEffectCloud extends CraftEntity implements AreaEffectCloud
             this.removeCustomEffect(effect.getType());
         }
         this.getHandle().addEffect(CraftPotionUtil.fromBukkit(effect));
-        this.getHandle().updateColor();
+        // this.getHandle().updateColor(); // Paper - already done above
         return true;
     }
 
@@ -151,7 +151,7 @@ public class CraftAreaEffectCloud extends CraftEntity implements AreaEffectCloud
     public void clearCustomEffects() {
         PotionContents old = this.getHandle().potionContents;
         this.getHandle().setPotionContents(new PotionContents(old.potion(), old.customColor(), List.of()));
-        this.getHandle().updateColor();
+        // this.getHandle().updateColor(); // Paper - already done above
     }
 
     @Override
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftArrow.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftArrow.java
index 5232fbef0d014edd32a5d18d4a1500ab215313f5..071be344c3265a0cd52b31ffbb02ff7a70bdf231 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftArrow.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftArrow.java
@@ -43,7 +43,7 @@ public class CraftArrow extends CraftAbstractArrow implements Arrow {
             this.removeCustomEffect(effect.getType());
         }
         this.getHandle().addEffect(CraftPotionUtil.fromBukkit(effect));
-        this.getHandle().updateColor();
+        // this.getHandle().updateColor(); // Paper - already done above
         return true;
     }
 
@@ -51,7 +51,7 @@ public class CraftArrow extends CraftAbstractArrow implements Arrow {
     public void clearCustomEffects() {
         PotionContents old = this.getHandle().getPotionContents();
         this.getHandle().setPotionContents(new PotionContents(old.potion(), old.customColor(), List.of()));
-        this.getHandle().updateColor();
+        // this.getHandle().updateColor(); // Paper - already done above
     }
 
     @Override
@@ -117,16 +117,17 @@ public class CraftArrow extends CraftAbstractArrow implements Arrow {
 
     @Override
     public void setColor(Color color) {
-        int colorRGB = (color == null) ? -1 : color.asRGB();
+        int colorRGB = (color == null) ? net.minecraft.world.entity.projectile.Arrow.NO_EFFECT_COLOR : color.asARGB(); // Paper
         PotionContents old = this.getHandle().getPotionContents();
         this.getHandle().setPotionContents(new PotionContents(old.potion(), Optional.of(colorRGB), old.customEffects()));
     }
 
     @Override
     public Color getColor() {
-        if (this.getHandle().getColor() <= -1) {
+        int color = this.getHandle().getColor(); // Paper
+        if (color == net.minecraft.world.entity.projectile.Arrow.NO_EFFECT_COLOR) { // Paper
             return null;
         }
-        return Color.fromRGB(this.getHandle().getColor());
+        return Color.fromARGB(color); // Paper
     }
 }
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntityTypes.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntityTypes.java
index 37527713b0afa6db19eefd57aaffcb2fe3544ce6..46a4f31e2b6eee6f8dc5f8fccd7de4c48a698b61 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntityTypes.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntityTypes.java
@@ -389,7 +389,7 @@ public final class CraftEntityTypes {
             BlockPos pos = BlockPos.containing(spawnData.x(), spawnData.y(), spawnData.z());
             return new FallingBlockEntity(spawnData.minecraftWorld(), spawnData.x(), spawnData.y(), spawnData.z(), spawnData.world().getBlockState(pos)); // Paper - create falling block entities correctly
         }));
-        register(new EntityTypeData<>(EntityType.FIREWORK_ROCKET, Firework.class, CraftFirework::new, spawnData -> new FireworkRocketEntity(spawnData.minecraftWorld(), spawnData.x(), spawnData.y(), spawnData.z(), net.minecraft.world.item.ItemStack.EMPTY)));
+        register(new EntityTypeData<>(EntityType.FIREWORK_ROCKET, Firework.class, CraftFirework::new, spawnData -> new FireworkRocketEntity(spawnData.minecraftWorld(), spawnData.x(), spawnData.y(), spawnData.z(), FireworkRocketEntity.getDefaultItem()))); // Paper - pass correct default to rocket for data storage
         register(new EntityTypeData<>(EntityType.EVOKER_FANGS, EvokerFangs.class, CraftEvokerFangs::new, spawnData -> new net.minecraft.world.entity.projectile.EvokerFangs(spawnData.minecraftWorld(), spawnData.x(), spawnData.y(), spawnData.z(), (float) Math.toRadians(spawnData.yaw()), 0, null)));
         register(new EntityTypeData<>(EntityType.COMMAND_BLOCK_MINECART, CommandMinecart.class, CraftMinecartCommand::new, spawnData -> new MinecartCommandBlock(spawnData.minecraftWorld(), spawnData.x(), spawnData.y(), spawnData.z())));
         register(new EntityTypeData<>(EntityType.MINECART, RideableMinecart.class, CraftMinecartRideable::new, spawnData -> new Minecart(spawnData.minecraftWorld(), spawnData.x(), spawnData.y(), spawnData.z())));
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftFireball.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftFireball.java
index 1b084d63bdbb24dad45d28eed1693eb6e26e24dc..43d7bea201a52cfeacf60c75caa28dfd2c4ff164 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftFireball.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftFireball.java
@@ -34,20 +34,7 @@ public class CraftFireball extends AbstractProjectile implements Fireball {
         this.getHandle().bukkitYield = yield;
     }
 
-    @Override
-    public ProjectileSource getShooter() {
-        return this.getHandle().projectileSource;
-    }
-
-    @Override
-    public void setShooter(ProjectileSource shooter) {
-        if (shooter instanceof CraftLivingEntity) {
-            this.getHandle().setOwner(((CraftLivingEntity) shooter).getHandle());
-        } else {
-            this.getHandle().setOwner(null);
-        }
-        this.getHandle().projectileSource = shooter;
-    }
+    // Paper - moved to AbstractProjectile
 
     @Override
     public Vector getDirection() {
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftFirework.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftFirework.java
index c9e15a9d82dee935293b2e7e233f5b9b2d822448..2d54cf6f3d9696c55335f0a2057025e2034d4e13 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftFirework.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftFirework.java
@@ -15,24 +15,26 @@ import org.bukkit.inventory.meta.FireworkMeta;
 public class CraftFirework extends CraftProjectile implements Firework {
 
     private final Random random = new Random();
-    private final CraftItemStack item;
+    //private CraftItemStack item; // Paper - Remove usage, not accurate representation of current item.
 
     public CraftFirework(CraftServer server, FireworkRocketEntity entity) {
         super(server, entity);
 
-        ItemStack item = this.getHandle().getEntityData().get(FireworkRocketEntity.DATA_ID_FIREWORKS_ITEM);
-
-        if (item.isEmpty()) {
-            item = new ItemStack(Items.FIREWORK_ROCKET);
-            this.getHandle().getEntityData().set(FireworkRocketEntity.DATA_ID_FIREWORKS_ITEM, item);
-        }
-
-        this.item = CraftItemStack.asCraftMirror(item);
-
-        // Ensure the item is a firework...
-        if (this.item.getType() != Material.FIREWORK_ROCKET) {
-            this.item.setType(Material.FIREWORK_ROCKET);
-        }
+        // Paper start - Expose firework item directly
+//        ItemStack item = this.getHandle().getEntityData().get(FireworkRocketEntity.DATA_ID_FIREWORKS_ITEM);
+//
+//        if (item.isEmpty()) {
+//            item = new ItemStack(Items.FIREWORK_ROCKET);
+//            this.getHandle().getEntityData().set(FireworkRocketEntity.DATA_ID_FIREWORKS_ITEM, item);
+//        }
+//
+//        this.item = CraftItemStack.asCraftMirror(item);
+//
+//        // Ensure the item is a firework...
+//        if (this.item.getType() != Material.FIREWORK_ROCKET) {
+//            this.item.setType(Material.FIREWORK_ROCKET);
+//        }
+        // Paper end - Expose firework item directly
     }
 
     @Override
@@ -47,12 +49,12 @@ public class CraftFirework extends CraftProjectile implements Firework {
 
     @Override
     public FireworkMeta getFireworkMeta() {
-        return (FireworkMeta) this.item.getItemMeta();
+        return (FireworkMeta) CraftItemStack.getItemMeta(this.getHandle().getEntityData().get(FireworkRocketEntity.DATA_ID_FIREWORKS_ITEM), org.bukkit.inventory.ItemType.FIREWORK_ROCKET); // Paper - Expose firework item directly
     }
 
     @Override
     public void setFireworkMeta(FireworkMeta meta) {
-        this.item.setItemMeta(meta);
+        applyFireworkEffect(meta); // Paper - Expose firework item directly
 
         // Copied from EntityFireworks constructor, update firework lifetime/power
         this.getHandle().lifetime = 10 * (1 + meta.getPower()) + this.random.nextInt(6) + this.random.nextInt(7);
@@ -136,4 +138,46 @@ public class CraftFirework extends CraftProjectile implements Firework {
         return getHandle().spawningEntity;
     }
     // Paper end
+    // Paper start - Expose firework item directly + manually setting flight
+    @Override
+    public org.bukkit.inventory.ItemStack getItem() {
+        return CraftItemStack.asBukkitCopy(this.getHandle().getItem());
+    }
+
+    @Override
+    public void setItem(org.bukkit.inventory.ItemStack itemStack) {
+        FireworkMeta meta = getFireworkMeta();
+        ItemStack nmsItem = itemStack == null ? FireworkRocketEntity.getDefaultItem() : CraftItemStack.asNMSCopy(itemStack);
+        this.getHandle().getEntityData().set(FireworkRocketEntity.DATA_ID_FIREWORKS_ITEM, nmsItem);
+
+        applyFireworkEffect(meta);
+    }
+
+    @Override
+    public int getTicksFlown() {
+        return this.getHandle().life;
+    }
+
+    @Override
+    public void setTicksFlown(int ticks) {
+        this.getHandle().life = ticks;
+    }
+
+    @Override
+    public int getTicksToDetonate() {
+        return this.getHandle().lifetime;
+    }
+
+    @Override
+    public void setTicksToDetonate(int ticks) {
+        this.getHandle().lifetime = ticks;
+    }
+
+    void applyFireworkEffect(FireworkMeta meta) {
+        ItemStack item = this.getHandle().getItem();
+        CraftItemStack.applyMetaToItem(item, meta);
+
+        this.getHandle().getEntityData().set(FireworkRocketEntity.DATA_ID_FIREWORKS_ITEM, item);
+    }
+    // Paper end - Expose firework item directly + manually setting flight
 }
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftFishHook.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftFishHook.java
index 6e2f91423371ead9890095cf4b1e2299c4dcba28..9d8f4b7176e60180565e3134a14ecf19060f2621 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftFishHook.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftFishHook.java
@@ -196,4 +196,42 @@ public class CraftFishHook extends CraftProjectile implements FishHook {
     public HookState getState() {
         return HookState.values()[this.getHandle().currentState.ordinal()];
     }
+    // Paper start - More FishHook API
+    @Override
+    public int getWaitTime() {
+        return this.getHandle().timeUntilLured;
+    }
+
+    @Override
+    public void setWaitTime(int ticks) {
+        this.getHandle().timeUntilLured = ticks;
+    }
+
+    @Override
+    public int getTimeUntilBite() {
+        return this.getHandle().timeUntilHooked;
+    }
+
+    @Override
+    public void setTimeUntilBite(final int ticks) {
+        com.google.common.base.Preconditions.checkArgument(ticks >= 1, "Cannot set time until bite to less than 1 (%s<1)", ticks);
+        final FishingHook hook = this.getHandle();
+
+        // Reset the fish angle hook only when this call "enters" the fish into the lure stage.
+        final boolean alreadyInLuringPhase = hook.timeUntilHooked > 0 && hook.timeUntilLured <= 0;
+        if (!alreadyInLuringPhase) {
+            hook.fishAngle = net.minecraft.util.Mth.nextFloat(hook.random, hook.minLureAngle, hook.maxLureAngle);
+            hook.timeUntilLured = 0;
+        }
+
+        hook.timeUntilHooked = ticks;
+    }
+
+    @Override
+    public void resetFishingState() {
+        final FishingHook hook = this.getHandle();
+        hook.resetTimeUntilLured();
+        hook.timeUntilHooked = 0; // Reset time until hooked, will be repopulated once lured time is ticked down.
+    }
+    // Paper end
 }
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java
index 0ed2910e64b6efdb4180c5bc23a146aced87c3d9..6350d4729fa3fbd9b15f1150cb4322f1c685044a 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java
@@ -570,8 +570,15 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity {
     }
 
     @Override
-    @SuppressWarnings("unchecked")
     public <T extends Projectile> T launchProjectile(Class<? extends T> projectile, Vector velocity) {
+        // Paper start - launchProjectile consumer
+        return this.launchProjectile(projectile, velocity, null);
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends Projectile> T launchProjectile(Class<? extends T> projectile, Vector velocity, java.util.function.Consumer<? super T> function) {
+        // Paper end - launchProjectile consumer
         Preconditions.checkState(!this.getHandle().generation, "Cannot launch projectile during world generation");
 
         net.minecraft.world.level.Level world = ((CraftWorld) this.getWorld()).getHandle();
@@ -597,7 +604,7 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity {
             } else {
                 launch = new net.minecraft.world.entity.projectile.Arrow(world, this.getHandle(), new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.ARROW), null);
             }
-            ((net.minecraft.world.entity.projectile.AbstractArrow) launch).shootFromRotation(this.getHandle(), this.getHandle().getXRot(), this.getHandle().getYRot(), 0.0F, 3.0F, 1.0F); // ItemBow
+            ((net.minecraft.world.entity.projectile.AbstractArrow) launch).shootFromRotation(this.getHandle(), this.getHandle().getXRot(), this.getHandle().getYRot(), 0.0F, Trident.class.isAssignableFrom(projectile) ? net.minecraft.world.item.TridentItem.SHOOT_POWER : 3.0F, 1.0F); // ItemBow // Paper - see TridentItem
         } else if (ThrownPotion.class.isAssignableFrom(projectile)) {
             if (LingeringPotion.class.isAssignableFrom(projectile)) {
                 launch = new net.minecraft.world.entity.projectile.ThrownPotion(world, this.getHandle());
@@ -656,8 +663,26 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity {
         } else if (Firework.class.isAssignableFrom(projectile)) {
             Location location = this.getEyeLocation();
 
-            launch = new FireworkRocketEntity(world, net.minecraft.world.item.ItemStack.EMPTY, this.getHandle());
-            launch.moveTo(location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch());
+            // Paper start - see CrossbowItem
+            launch = new FireworkRocketEntity(world, FireworkRocketEntity.getDefaultItem(), this.getHandle(), location.getX(), location.getY() - 0.15F, location.getZ(), true); // Paper - pass correct default to rocket for data storage & see CrossbowItem for regular launch without elytra boost
+
+            // Lifted from net.minecraft.world.item.ProjectileWeaponItem.shoot
+            float f2 = /* net.minecraft.world.item.enchantment.EnchantmentHelper.processProjectileSpread((ServerLevel) world, new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.CROSSBOW), this.getHandle(), 0.0F); */ 0; // Just shortcut this to 0, no need to do any calculations on a non existing stack
+            int projectileSize = 1;
+            int i = 0;
+
+            float f3 = projectileSize == 1 ? 0.0F : 2.0F * f2 / (float) (projectileSize - 1);
+            float f4 = (float) ((projectileSize - 1) % 2) * f3 / 2.0F;
+            float f5 = 1.0F;
+            float yaw = f4 + f5 * (float) ((i + 1) / 2) * f3;
+
+            // Lifted from net.minecraft.world.item.CrossbowItem.shootProjectile
+            Vec3 vec3 = this.getHandle().getUpVector(1.0F);
+            org.joml.Quaternionf quaternionf = new org.joml.Quaternionf().setAngleAxis((double)(yaw * (float) (Math.PI / 180.0)), vec3.x, vec3.y, vec3.z);
+            Vec3 vec32 = this.getHandle().getViewVector(1.0F);
+            org.joml.Vector3f vector3f = vec32.toVector3f().rotate(quaternionf);
+            ((FireworkRocketEntity) launch).shoot((double)vector3f.x(), (double)vector3f.y(), (double)vector3f.z(), net.minecraft.world.item.CrossbowItem.FIREWORK_POWER, 1.0F);
+            // Paper end
         }
 
         Preconditions.checkArgument(launch != null, "Projectile (%s) not supported", projectile.getName());
@@ -665,6 +690,11 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity {
         if (velocity != null) {
             ((T) launch.getBukkitEntity()).setVelocity(velocity);
         }
+        // Paper start - launchProjectile consumer
+        if (function != null) {
+            function.accept((T) launch.getBukkitEntity());
+        }
+        // Paper end - launchProjectile consumer
 
         world.addFreshEntity(launch);
         return (T) launch.getBukkitEntity();
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftLlamaSpit.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftLlamaSpit.java
index 70cbc6c668c60e9d608ca7013b72f9b916c05c2d..47633f05b4fab1dcabc2117e7645fe6d6949622a 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftLlamaSpit.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftLlamaSpit.java
@@ -20,13 +20,5 @@ public class CraftLlamaSpit extends AbstractProjectile implements LlamaSpit {
         return "CraftLlamaSpit";
     }
 
-    @Override
-    public ProjectileSource getShooter() {
-        return (this.getHandle().getOwner() != null) ? (ProjectileSource) this.getHandle().getOwner().getBukkitEntity() : null;
-    }
-
-    @Override
-    public void setShooter(ProjectileSource source) {
-        this.getHandle().setOwner((source != null) ? ((CraftLivingEntity) source).getHandle() : null);
-    }
+    // Paper - moved to AbstractProjectile
 }
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftProjectile.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftProjectile.java
index 696fdfa723aa896a67946f862d7c6890f7f7ab17..4f1fa7dec78970bdfc184d3c1f1632dc9d75a574 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftProjectile.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftProjectile.java
@@ -10,20 +10,7 @@ public abstract class CraftProjectile extends AbstractProjectile implements Proj
         super(server, entity);
     }
 
-    @Override
-    public ProjectileSource getShooter() {
-        return this.getHandle().projectileSource;
-    }
-
-    @Override
-    public void setShooter(ProjectileSource shooter) {
-        if (shooter instanceof CraftLivingEntity) {
-            this.getHandle().setOwner((LivingEntity) ((CraftLivingEntity) shooter).entity);
-        } else {
-            this.getHandle().setOwner(null);
-        }
-        this.getHandle().projectileSource = shooter;
-    }
+    // Paper - moved to AbstractProjectile
 
     @Override
     public net.minecraft.world.entity.projectile.Projectile getHandle() {
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftShulkerBullet.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftShulkerBullet.java
index d685d09cae5f862c0004f148298c800736d2139e..b3797a43eeee11cb7ae0774d61bd5f195d0aa3ad 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftShulkerBullet.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftShulkerBullet.java
@@ -12,31 +12,56 @@ public class CraftShulkerBullet extends AbstractProjectile implements ShulkerBul
         super(server, entity);
     }
 
+    // Paper - moved to AbstractProjectile
+
+    @Override
+    public org.bukkit.entity.Entity getTarget() {
+        return this.getHandle().getTarget() != null ? this.getHandle().getTarget().getBukkitEntity() : null;
+    }
+
     @Override
-    public ProjectileSource getShooter() {
-        return this.getHandle().projectileSource;
+    public void setTarget(org.bukkit.entity.Entity target) {
+        Preconditions.checkState(!this.getHandle().generation, "Cannot set target during world generation");
+
+        this.getHandle().setTarget(target == null ? null : ((CraftEntity) target).getHandle());
     }
 
     @Override
-    public void setShooter(ProjectileSource shooter) {
-        if (shooter instanceof Entity) {
-            this.getHandle().setOwner(((CraftEntity) shooter).getHandle());
-        } else {
-            this.getHandle().setOwner(null);
+    public org.bukkit.util.Vector getTargetDelta() {
+        net.minecraft.world.entity.projectile.ShulkerBullet bullet = this.getHandle();
+        return new org.bukkit.util.Vector(bullet.targetDeltaX, bullet.targetDeltaY, bullet.targetDeltaZ);
+    }
+
+    @Override
+    public void setTargetDelta(org.bukkit.util.Vector vector) {
+        net.minecraft.world.entity.projectile.ShulkerBullet bullet = this.getHandle();
+        bullet.targetDeltaX = vector.getX();
+        bullet.targetDeltaY = vector.getY();
+        bullet.targetDeltaZ = vector.getZ();
+    }
+
+    @Override
+    public org.bukkit.block.BlockFace getCurrentMovementDirection() {
+        net.minecraft.core.Direction dir = this.getHandle().currentMoveDirection;
+        if (dir == null) {
+            return null; // random dir
         }
-        this.getHandle().projectileSource = shooter;
+        return org.bukkit.craftbukkit.block.CraftBlock.notchToBlockFace(dir);
     }
 
     @Override
-    public org.bukkit.entity.Entity getTarget() {
-        return this.getHandle().getTarget() != null ? this.getHandle().getTarget().getBukkitEntity() : null;
+    public void setCurrentMovementDirection(org.bukkit.block.BlockFace movementDirection) {
+        this.getHandle().currentMoveDirection = org.bukkit.craftbukkit.block.CraftBlock.blockFaceToNotch(movementDirection);
     }
 
     @Override
-    public void setTarget(org.bukkit.entity.Entity target) {
-        Preconditions.checkState(!this.getHandle().generation, "Cannot set target during world generation");
+    public int getFlightSteps() {
+        return this.getHandle().flightSteps;
+    }
 
-        this.getHandle().setTarget(target == null ? null : ((CraftEntity) target).getHandle());
+    @Override
+    public void setFlightSteps(int steps) {
+        this.getHandle().flightSteps = steps;
     }
 
     @Override
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftThrownPotion.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftThrownPotion.java
index d67a80161b3e7c1fe02a6ed9d341c00dc7c2847a..f6fa6f1ac50b757dd3bc9a8dee9f6085446182c8 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftThrownPotion.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftThrownPotion.java
@@ -36,11 +36,31 @@ public class CraftThrownPotion extends CraftThrowableProjectile implements Throw
     @Override
     public void setItem(ItemStack item) {
         Preconditions.checkArgument(item != null, "ItemStack cannot be null");
-        Preconditions.checkArgument(item.getType() == Material.LINGERING_POTION || item.getType() == Material.SPLASH_POTION, "ItemStack material must be Material.LINGERING_POTION or Material.SPLASH_POTION but was Material.%s", item.getType());
+        // Preconditions.checkArgument(item.getType() == Material.LINGERING_POTION || item.getType() == Material.SPLASH_POTION, "ItemStack material must be Material.LINGERING_POTION or Material.SPLASH_POTION but was Material.%s", item.getType()); // Paper - Projectile API
+        org.bukkit.inventory.meta.PotionMeta meta = (item.getType() == Material.LINGERING_POTION || item.getType() == Material.SPLASH_POTION) ? null : this.getPotionMeta(); // Paper - Projectile API
 
         this.getHandle().setItem(CraftItemStack.asNMSCopy(item));
+        if (meta != null) this.setPotionMeta(meta); // Paper - Projectile API
     }
 
+    // Paper start - Projectile API
+    @Override
+    public org.bukkit.inventory.meta.PotionMeta getPotionMeta() {
+        return (org.bukkit.inventory.meta.PotionMeta) CraftItemStack.getItemMeta(this.getHandle().getItem(), org.bukkit.inventory.ItemType.SPLASH_POTION);
+    }
+
+    @Override
+    public void setPotionMeta(org.bukkit.inventory.meta.PotionMeta meta) {
+        net.minecraft.world.item.ItemStack item = this.getHandle().getItem();
+        CraftItemStack.applyMetaToItem(item, meta);
+        this.getHandle().setItem(item); // Reset item
+    }
+
+    @Override
+    public void splash() {
+        this.getHandle().splash(null);
+    }
+    // Paper end
     @Override
     public net.minecraft.world.entity.projectile.ThrownPotion getHandle() {
         return (net.minecraft.world.entity.projectile.ThrownPotion) this.entity;
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftTrident.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftTrident.java
index e374b9f40eddca13b30855d25a2030f8df98138f..4fc893378fb0568ddcffc7593d66df6bfe23f659 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftTrident.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftTrident.java
@@ -53,5 +53,15 @@ public class CraftTrident extends CraftAbstractArrow implements Trident {
         com.google.common.base.Preconditions.checkArgument(loyaltyLevel >= 0 && loyaltyLevel <= 127, "The loyalty level has to be between 0 and 127");
         this.getHandle().setLoyalty((byte) loyaltyLevel);
     }
+
+    @Override
+    public boolean hasDealtDamage() {
+        return this.getHandle().dealtDamage;
+    }
+
+    @Override
+    public void setHasDealtDamage(boolean hasDealtDamage) {
+        this.getHandle().dealtDamage = hasDealtDamage;
+    }
     // Paper end
 }
diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
index 5fc6ef13cdc9df11b0fd2b0baf3cec862ccb5e37..eeb326a115020f571e96f3ec85408950b5eb56fb 100644
--- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
+++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
@@ -843,19 +843,19 @@ public class CraftEventFactory {
     /**
      * PotionSplashEvent
      */
-    public static PotionSplashEvent callPotionSplashEvent(net.minecraft.world.entity.projectile.ThrownPotion potion, HitResult position, Map<LivingEntity, Double> affectedEntities) {
+    public static PotionSplashEvent callPotionSplashEvent(net.minecraft.world.entity.projectile.ThrownPotion potion, @Nullable HitResult position, Map<LivingEntity, Double> affectedEntities) { // Paper - nullable hitResult
         ThrownPotion thrownPotion = (ThrownPotion) potion.getBukkitEntity();
 
         Block hitBlock = null;
         BlockFace hitFace = null;
-        if (position.getType() == HitResult.Type.BLOCK) {
+        if (position != null && position.getType() == HitResult.Type.BLOCK) { // Paper - nullable hitResult
             BlockHitResult positionBlock = (BlockHitResult) position;
             hitBlock = CraftBlock.at(potion.level(), positionBlock.getBlockPos());
             hitFace = CraftBlock.notchToBlockFace(positionBlock.getDirection());
         }
 
         org.bukkit.entity.Entity hitEntity = null;
-        if (position.getType() == HitResult.Type.ENTITY) {
+        if (position != null && position.getType() == HitResult.Type.ENTITY) { // Paper - nullable hitResult
             hitEntity = ((EntityHitResult) position).getEntity().getBukkitEntity();
         }
 
@@ -864,20 +864,20 @@ public class CraftEventFactory {
         return event;
     }
 
-    public static LingeringPotionSplashEvent callLingeringPotionSplashEvent(net.minecraft.world.entity.projectile.ThrownPotion potion, HitResult position, net.minecraft.world.entity.AreaEffectCloud cloud) {
+    public static LingeringPotionSplashEvent callLingeringPotionSplashEvent(net.minecraft.world.entity.projectile.ThrownPotion potion, @Nullable HitResult position, net.minecraft.world.entity.AreaEffectCloud cloud) { // Paper - nullable hitResult
         ThrownPotion thrownPotion = (ThrownPotion) potion.getBukkitEntity();
         AreaEffectCloud effectCloud = (AreaEffectCloud) cloud.getBukkitEntity();
 
         Block hitBlock = null;
         BlockFace hitFace = null;
-        if (position.getType() == HitResult.Type.BLOCK) {
+        if (position != null && position.getType() == HitResult.Type.BLOCK) { // Paper
             BlockHitResult positionBlock = (BlockHitResult) position;
             hitBlock = CraftBlock.at(potion.level(), positionBlock.getBlockPos());
             hitFace = CraftBlock.notchToBlockFace(positionBlock.getDirection());
         }
 
         org.bukkit.entity.Entity hitEntity = null;
-        if (position.getType() == HitResult.Type.ENTITY) {
+        if (position != null && position.getType() == HitResult.Type.ENTITY) { // Paper
             hitEntity = ((EntityHitResult) position).getEntity().getBukkitEntity();
         }
 
diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
index e8a455eb5e17bcfcae3f03664f2b47773fbdf37e..08178a88ba7d0881a6c2843eef24a846cf07adb4 100644
--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
+++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
@@ -329,12 +329,23 @@ public final class CraftItemStack extends ItemStack {
     public ItemMeta getItemMeta() {
         return CraftItemStack.getItemMeta(this.handle);
     }
+    // Paper start
+    public static void applyMetaToItem(net.minecraft.world.item.ItemStack itemStack, ItemMeta itemMeta) {
+        final CraftMetaItem.Applicator tag = new CraftMetaItem.Applicator();
+        ((CraftMetaItem) itemMeta).applyToItem(tag);
+        itemStack.applyComponents(tag.build());
+    }
 
     public static ItemMeta getItemMeta(net.minecraft.world.item.ItemStack item) {
+        return getItemMeta(item, null);
+    }
+    public static ItemMeta getItemMeta(net.minecraft.world.item.ItemStack item, org.bukkit.inventory.ItemType metaForType) {
+        // Paper end
         if (!CraftItemStack.hasItemMeta(item)) {
             return CraftItemFactory.instance().getItemMeta(CraftItemStack.getType(item));
         }
 
+        if (metaForType != null) { return ((CraftItemType<?>) metaForType).getItemMeta(item); } // Paper
         return ((CraftItemType<?>) CraftItemType.minecraftToBukkitNew(item.getItem())).getItemMeta(item);
     }
 
diff --git a/src/main/java/org/bukkit/craftbukkit/projectiles/CraftBlockProjectileSource.java b/src/main/java/org/bukkit/craftbukkit/projectiles/CraftBlockProjectileSource.java
index 93b9e6b32798e32026492fa3f80de9d4a7b3d9a5..baab2e097777a4295e7b14957304c927640a4a77 100644
--- a/src/main/java/org/bukkit/craftbukkit/projectiles/CraftBlockProjectileSource.java
+++ b/src/main/java/org/bukkit/craftbukkit/projectiles/CraftBlockProjectileSource.java
@@ -55,7 +55,15 @@ public class CraftBlockProjectileSource implements BlockProjectileSource {
 
     @Override
     public <T extends Projectile> T launchProjectile(Class<? extends T> projectile, Vector velocity) {
+        // Paper start - launchProjectile consumer
+        return this.launchProjectile(projectile, velocity, null);
+    }
+
+    @Override
+    public <T extends Projectile> T launchProjectile(Class<? extends T> projectile, Vector velocity, java.util.function.Consumer<? super T> function) {
+        // Paper end - launchProjectile consumer
         Preconditions.checkArgument(this.getBlock().getType() == Material.DISPENSER, "Block is no longer dispenser");
+
         // Copied from BlockDispenser.dispense()
         BlockSource sourceblock = new BlockSource((ServerLevel) this.dispenserBlock.getLevel(), this.dispenserBlock.getBlockPos(), this.dispenserBlock.getBlockState(), this.dispenserBlock);
         // Copied from DispenseBehaviorProjectile
@@ -88,7 +96,7 @@ public class CraftBlockProjectileSource implements BlockProjectileSource {
         } else if (Fireball.class.isAssignableFrom(projectile)) {
             if (AbstractWindCharge.class.isAssignableFrom(projectile)) {
                 item = Items.WIND_CHARGE;
-            } else {
+            } else if (org.bukkit.entity.SmallFireball.class.isAssignableFrom(projectile)) { // Paper - more projectile API - only allow firing fire charges.
                 item = Items.FIRE_CHARGE;
             }
         } else if (Firework.class.isAssignableFrom(projectile)) {
@@ -104,7 +112,7 @@ public class CraftBlockProjectileSource implements BlockProjectileSource {
         Position iposition = dispenseConfig.positionFunction().getDispensePosition(sourceblock, enumdirection);
         net.minecraft.world.entity.projectile.Projectile launch = projectileItem.asProjectile(world, iposition, itemstack, enumdirection);
 
-        if (Fireball.class.isAssignableFrom(projectile)) {
+        if (false && Fireball.class.isAssignableFrom(projectile)) { // Paper - more project API - dispensers cannot launch anything but fire charges.
             AbstractHurtingProjectile customFireball = null;
             if (WitherSkull.class.isAssignableFrom(projectile)) {
                 launch = customFireball = EntityType.WITHER_SKULL.create(world);
@@ -129,7 +137,7 @@ public class CraftBlockProjectileSource implements BlockProjectileSource {
             }
         }
 
-        if (launch instanceof net.minecraft.world.entity.projectile.AbstractArrow arrow) {
+        if (false && launch instanceof net.minecraft.world.entity.projectile.AbstractArrow arrow) { // Paper - more projectile API - this is set by the respective ArrowItem when constructing the projectile
             arrow.pickup = net.minecraft.world.entity.projectile.AbstractArrow.Pickup.ALLOWED;
         }
         launch.projectileSource = this;
@@ -138,6 +146,11 @@ public class CraftBlockProjectileSource implements BlockProjectileSource {
         if (velocity != null) {
             ((T) launch.getBukkitEntity()).setVelocity(velocity);
         }
+        // Paper start
+        if (function != null) {
+            function.accept((T) launch.getBukkitEntity());
+        }
+        // Paper end
 
         world.addFreshEntity(launch);
         return (T) launch.getBukkitEntity();