aboutsummaryrefslogtreecommitdiffhomepage
path: root/patches/server/0666-More-Projectile-API.patch
diff options
context:
space:
mode:
Diffstat (limited to 'patches/server/0666-More-Projectile-API.patch')
-rw-r--r--patches/server/0666-More-Projectile-API.patch932
1 files changed, 932 insertions, 0 deletions
diff --git a/patches/server/0666-More-Projectile-API.patch b/patches/server/0666-More-Projectile-API.patch
new file mode 100644
index 0000000000..c03e65b770
--- /dev/null
+++ b/patches/server/0666-More-Projectile-API.patch
@@ -0,0 +1,932 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Owen1212055 <[email protected]>
+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 ownerUUID
+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 <[email protected]>
+Co-authored-by: SoSeDiK <[email protected]>
+Co-authored-by: MelnCat <[email protected]>
+Co-authored-by: Lulu13022002 <[email protected]>
+
+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 536196a740f607adda2a5ae7f644981ac26bef98..1f95234c0a1457050574aa0f6c4b2a8c91b1f272 100644
+--- a/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java
++++ b/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java
+@@ -419,13 +419,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 0e4ba92d97998937ccb83ebd60aaad3fb68f7546..01684c903930b14a0df3a146176e2b476a374efb 100644
+--- a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java
++++ b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java
+@@ -288,7 +288,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 d6ac07d9d5ee0430a1d91b7084b378aac1d047e5..a486466040a646b8a5a5ff2430cdd25b95b7e20f 100644
+--- a/src/main/java/net/minecraft/world/entity/projectile/ThrownPotion.java
++++ b/src/main/java/net/minecraft/world/entity/projectile/ThrownPotion.java
+@@ -103,8 +103,12 @@ public class ThrownPotion extends ThrowableItemProjectile {
+ @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
+ Level world = this.level();
+-
+ if (world instanceof ServerLevel worldserver) {
+ ItemStack itemstack = this.getItem();
+ PotionContents potioncontents = (PotionContents) itemstack.getOrDefault(DataComponents.POTION_CONTENTS, PotionContents.EMPTY);
+@@ -116,7 +120,7 @@ public class ThrownPotion extends ThrowableItemProjectile {
+ if (this.isLingering()) {
+ showParticles = this.makeAreaOfEffectCloud(potioncontents, hitResult); // CraftBukkit - Pass MovingObjectPosition // Paper
+ } else {
+- showParticles = this.applySplash(worldserver, potioncontents.getAllEffects(), hitResult.getType() == HitResult.Type.ENTITY ? ((EntityHitResult) hitResult).getEntity() : null, hitResult); // CraftBukkit - Pass MovingObjectPosition // Paper
++ showParticles = this.applySplash(worldserver, potioncontents.getAllEffects(), hitResult != null && hitResult.getType() == HitResult.Type.ENTITY ? ((EntityHitResult) hitResult).getEntity() : null, hitResult); // CraftBukkit - Pass MovingObjectPosition // Paper - More projectile API
+ }
+ }
+
+@@ -178,7 +182,7 @@ public class ThrownPotion extends ThrowableItemProjectile {
+
+ }
+
+- private boolean applySplash(ServerLevel worldserver, Iterable<MobEffectInstance> iterable, @Nullable Entity entity, HitResult position) { // CraftBukkit - Pass MovingObjectPosition // Paper - Fix potions splash events
++ private boolean applySplash(ServerLevel worldserver, 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 = worldserver.getEntitiesOfClass(net.minecraft.world.entity.LivingEntity.class, axisalignedbb);
+ Map<LivingEntity, Double> affected = new HashMap<LivingEntity, Double>(); // CraftBukkit
+@@ -256,7 +260,7 @@ public class ThrownPotion extends ThrowableItemProjectile {
+
+ }
+
+- 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 0f85c1f991469b277bba8b40b087f7224b4b3a85..1f30109abd86b76af343eb5eb75ec3db83ef9417 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractArrow.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractArrow.java
+@@ -59,20 +59,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() {
+@@ -133,6 +120,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());
+ }
+
+@@ -152,4 +140,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 6591513bb62226b6f85fd2ef9f6ebe376f0f7362..f9c113dc018702159345240d6d0de85767afa0c3 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(), old.customName()));
+- 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 199d5836dc787cca54c6b653a4e67573f2f758a2..15d50a284cafc2eb59239ca00926836526f09e06 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(), old.customName()));
+- 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(), old.customName()));
+ }
+
+ @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 0bafd3b1a55154c8e9eb37c96df9f5985640a675..0b9c4f9b61651660e821735f18b4f3cc7cfd164c 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntityTypes.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntityTypes.java
+@@ -442,7 +442,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, createMinecart(net.minecraft.world.entity.EntityType.COMMAND_BLOCK_MINECART)));
+ register(new EntityTypeData<>(EntityType.MINECART, RideableMinecart.class, CraftMinecartRideable::new, createMinecart(net.minecraft.world.entity.EntityType.MINECART)));
+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 e148239d4930e5cbb000beed4de386f992f28d88..14b4c3835388d957653ba34444968bb718ce7f68 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java
+@@ -579,8 +579,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();
+@@ -606,7 +613,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(), new net.minecraft.world.item.ItemStack(Items.LINGERING_POTION));
+@@ -663,8 +670,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());
+@@ -672,6 +697,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 a5285a8952b2d99bfbb928b1bb31d3ddcf8ed57b..ca094e393de32b64db59c9fe906433761d70d29b 100644
+--- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
++++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
+@@ -841,19 +841,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();
+ }
+
+@@ -862,20 +862,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 60062ea5b18b95a14c459f2f3a0743c1e1ac0f12..502be683e8b04a9966043c9bee9d9fe793b12ef5 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
+@@ -341,12 +341,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 642f5bf75661eb485558bc227f668e84416f3b5f..76fd4d27730d9139caa67099a6757ea33d681be9 100644
+--- a/src/main/java/org/bukkit/craftbukkit/projectiles/CraftBlockProjectileSource.java
++++ b/src/main/java/org/bukkit/craftbukkit/projectiles/CraftBlockProjectileSource.java
+@@ -56,7 +56,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
+@@ -68,7 +76,7 @@ public class CraftBlockProjectileSource implements BlockProjectileSource {
+ item = Items.SNOWBALL;
+ } else if (Egg.class.isAssignableFrom(projectile)) {
+ item = Items.EGG;
+- } else if (EnderPearl.class.isAssignableFrom(projectile)) {
++ } else if (false && EnderPearl.class.isAssignableFrom(projectile)) { // Paper - more projectile API - disallow enderpearl, it is not a projectile item
+ item = Items.ENDER_PEARL;
+ } else if (ThrownExpBottle.class.isAssignableFrom(projectile)) {
+ item = Items.EXPERIENCE_BOTTLE;
+@@ -83,20 +91,20 @@ public class CraftBlockProjectileSource implements BlockProjectileSource {
+ item = Items.TIPPED_ARROW;
+ } else if (SpectralArrow.class.isAssignableFrom(projectile)) {
+ item = Items.SPECTRAL_ARROW;
+- } else {
++ } else if (org.bukkit.entity.Arrow.class.isAssignableFrom(projectile)) { // Paper - more projectile API - disallow trident
+ item = Items.ARROW;
+ }
+ } else if (Fireball.class.isAssignableFrom(projectile)) {
+- if (AbstractWindCharge.class.isAssignableFrom(projectile)) {
++ if (org.bukkit.entity.WindCharge.class.isAssignableFrom(projectile)) { // Paper - more projectile API - only allow wind charge not breeze wind charge
+ 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)) {
+ item = Items.FIREWORK_ROCKET;
+ }
+
+- Preconditions.checkArgument(item instanceof ProjectileItem, "Projectile not supported");
++ Preconditions.checkArgument(item instanceof ProjectileItem, "Projectile '%s' not supported", projectile.getSimpleName()); // Paper - more projectile API - include simple name in exception
+
+ ItemStack itemstack = new ItemStack(item);
+ ProjectileItem projectileItem = (ProjectileItem) item;
+@@ -105,7 +113,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, EntitySpawnReason.TRIGGERED);
+@@ -130,7 +138,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;
+@@ -139,6 +147,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();