Skip to content

Conversation

@posterhusky
Copy link

  • made ProjectileHitEvent cancellable
  • new ProjectileHitBlockEvent extending ProjectileHitEvent with Block hitBlock and BlockFace hitFace
  • new ProjectileHitEntityEvent extending ProjectileHitEvent with Entity hitEntity
  • deprecated ProjectileCollideEvent beacuse of the new ProjectileHitEntityEvent
  • reworked the projectiles' collision calculation to call events and allow noclipping through blocks and entities

⚠ The ProjectileHitEvent now fires before the collision (like in new mc versions) and not after the projectile collided and died.

Copy link
Member

@roccodev roccodev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you and nice work!

I left some notes to simplify the implementation, however I'd also like to highlight something important with the design:

As you mentioned this changes the order of events, so I don't think it makes sense to reuse the existing ProjectileHitEvent, doing so would also break plugins designed for 1.8.

Instead, I'd create a new base event class and call it something like PreHit.


private int d = -1;
private int e = -1;
@@ -151,6 +155,49 @@ public class EntityArrow extends Entity implements IProjectile {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't this whole patch simply be:

diff --git a/src/main/java/net/minecraft/server/EntityArrow.java b/src/main/java/net/minecraft/server/EntityArrow.java
--- a/src/main/java/net/minecraft/server/EntityArrow.java	(revision fcd4bd805787519acff9ae7a6f70b646a21dc287)
+++ b/src/main/java/net/minecraft/server/EntityArrow.java	(date 1768936446917)
@@ -265,6 +265,11 @@
                 ProjectileCollideEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callProjectileCollideEvent(this, movingobjectposition.entity);
                 if (event.isCancelled()) movingobjectposition = null;
             }
+
+            if (movingobjectposition != null && movingobjectposition.entity == null) {
+                ProjectileHitBlockEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callProjectileHitBlockEvent(this, movingobjectposition);
+                if (event.isCancelled()) movingobjectposition = null;
+            }
             // KigPaper end
 
             if (movingobjectposition != null) {

I don't think the separate function is needed, it also had some issues, for example:

  • The 5 ticks lived check for self damage should check for this.as, not this.ar (which is the ground stuck timer)
  • If an entity collision fails it should move to block collision, not step out of the function

Also, the first part of the arrow tick logic checks for the block the arrow is in to be non-air and non-passable to stick the arrow to it. Wouldn't that cause arrows with speed < 1 block/tick to still hit blocks even if events are cancelled?


private int e = -1;
private int f = -1;
@@ -64,6 +68,46 @@ public abstract class EntityFireball extends Entity {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as for arrows, I guess

import java.util.List;
@@ -12,6 +14,8 @@ import java.util.UUID;

public abstract class EntityProjectile extends Entity implements IProjectile {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the simple arrow patch works here too

@@ -14,6 +16,8 @@ import java.util.List;
// CraftBukkit end

public class EntityFishingHook extends Entity {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For fishing hooks, you could also use the simple patch while also setting a flag if the event is cancelled that allows you to noclip the hook.

@roccodev
Copy link
Member

I'd also add docs to the events, for example I think if you set the block type in a hit-block listener to one that the projectile would pass through, you still need to cancel the event

@posterhusky
Copy link
Author

I made a separate function bcs i was acc thinking of doing a more complex implementation with ranked collisions, however i thought it would add too much complexity, especially for my inital goal of just being able to know what block was hit by the projectile.
Suppose the following scenario: a projectile travels fast enough to go through multiple blocks in 1 tick, and you have a listener that cancels collisions with green blocks only:
image
The current "basic" implementation would call the event once and ignore all the subsequent block collisions. i had an idea in mind to make a list of entities ranked by distance (squared) and make a while loop that goes through each collision recasting a ray for each cancelled block. here's a quick pesudeo code of how such an algorithm would work:

var block: MovingObjectPosition? = // hit block or null
var bDistSquared = // dist squared or something else (null, 0.0, or any value)
val collisionList: MutableList<Pair<Double, MovingObjectPosition>>

// same code here except all entities are stored in the list

var i = 0
val ignoredBlockList = mutableListOf()
while (i < collisionList.size || block != null) {
    if (block == null || collisionList[i].first < bDistSquared) { // if the next collision is an entity
        // call ProjectilePreHitEntityEvent
        if (!event.isCancelled) return collisionList[i].second
        i++
        continue
    }
    // call ProjectilePreHitBlockEvent
    if (!event.isCancelled) return block
    ignoredBlockList.append(block)
    (block, bDistSquared) = // recast ray ingoring blocks in ignoredBlockList and get distance
}
return null

the algorithm would also require the modification of the raytracing function to add support for ignored blocs so idk if this whole thing is worth the effort

@roccodev
Copy link
Member

I think that could be done by plugins instead. When the pre-hit and hit events are fired, you can access the motion vector with getVelocity(), so you can re-calculate manually. NMS would also be an option

@posterhusky
Copy link
Author

  • made patches simpler
  • added new ProjectilePreHitEvent and restored ProjectileHitEvent's old functionality
  • also added some docs idk if elaboration is needed tho

@posterhusky posterhusky requested a review from roccodev January 21, 2026 00:48
Comment on lines 61 to 67
- // KigPaper start
- if (movingobjectposition != null && movingobjectposition.entity != null) {
- ProjectileCollideEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callProjectileCollideEvent(this, movingobjectposition.entity);
- if (event.isCancelled()) movingobjectposition = null;
- }
- // KigPaper end
+ // KigPaper - moved ProjectileCollideEvent
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is moving needed here? What I suggested instead was to move the PreHitBlockEvent below this one. I think it's better to get the block data after it may have been potentially modified by the event... (then if for example plugins don't want the new block to be hit by the projectile, e.g. .setType(Material.WOOD_BUTTON), they can delay it by a tick).
Though I don't think this was an intentional change because the other projectile classes do get the data after the event has fired.

As a side note the // KigPaper comments are only useful in the context of the patched code (i.e. after all patches have been applied), ProjectileCollideEvent is a KigPaper API so the comment would not be needed here

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved it bcs it simplified the following:

If an entity collision fails it should move to block collision, not step out of the function

otherwise i'd have to create 2 variables to store both potential collisions bcs the entity collision overwrites the block collsion
what im currently doing is calling the event even before the overwrite happens, if its cancelled i do nothing (potential block collision remains in movingobjectposition), otherwise i proceed with the overwrite.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the block data does not affect the projectile's physics, it is only used to tell what block is the arrow stuck in and for special interactions (like priming tnt with flame bow).
i mainly changed this to prevent arrows being stuck mid-air: the next tick it will compare the new block data with the old one and if the event changed the block it will realise this and recalculate collisions
for special interactions imo it makes sense to apply to the old block, and the a() function can be ez called for the new block by the plugin with nms

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also is modifying 0187-Add-ProjectileCollideEvent.patch an option? Only the craft event factory changes can be left

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is for latest Paper but the steps described here should still work: https://github.com/PaperMC/Paper/blob/main/CONTRIBUTING.md#modifying-larger-feature-patches

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved it bcs it simplified the following:

If an entity collision fails it should move to block collision, not step out of the function

otherwise i'd have to create 2 variables to store both potential collisions bcs the entity collision overwrites the block collsion
what im currently doing is calling the event even before the overwrite happens, if its cancelled i do nothing (potential block collision remains in movingobjectposition), otherwise i proceed with the overwrite.

Checking the source again I realised the vanilla server doesn't move on to block collision when ignoring the entity (e.g. when the entity is invulnerable or on the same team as the shooter, that's a vanilla check)

Comment on lines 75 to 76
- IBlockData iblockdata1 = this.world.getType(blockposition1);
+ IBlockData iblockdata1 = hitBlockData == null ? this.world.getType(blockposition1) : hitBlockData; // KigPaper - use block before potential change by ProjectileHitBlockEvent
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...so this change is probably not needed here (and the hitBlockData variable can be removed)

}
}

+ ProjectilePreHitEvent event; // KigPaper
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scope here is unnecessary. While this is probably cleaner, in KigPaper we prefer to keep patches simpler and with minimal modifications to vanilla code

Comment on lines 104 to 114
- movingobjectposition = new MovingObjectPosition(entity);
+ // KigPaper start
+ event = CraftEventFactory.callProjectileCollideEvent(this, entity);
+ if (!event.isCancelled()) movingobjectposition = new MovingObjectPosition(entity);
+ // KigPaper end
}

// KigPaper start
- if (movingobjectposition != null && movingobjectposition.entity != null) {
- ProjectileCollideEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callProjectileCollideEvent(this, movingobjectposition.entity);
- if (event.isCancelled()) movingobjectposition = null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I understand these are equivalent (even when it comes to getting the block data before/after the event), but leaving the CollideEvent call where it was results in a simpler patch.

Comment on lines 144 to 178
+ ProjectilePreHitEvent event; // KigPaper
+
if (entity != null) {
- movingobjectposition = new MovingObjectPosition(entity);
+ // KigPaper start
+ event = CraftEventFactory.callProjectileCollideEvent(this, entity);
+ if (!event.isCancelled()) movingobjectposition = new MovingObjectPosition(entity);
+ // KigPaper end
+ }
+
+ // KigPaper start
+ // KigPaper - allow clipping though ground when ProjectileHitBlockEvent is cancelled
+ boolean isBlockEventCancelled = false;
+ if (movingobjectposition != null && movingobjectposition.entity == null) {
+ event = CraftEventFactory.callProjectilePreHitBlockEvent(this, movingobjectposition);
+ if (event.isCancelled()) {
+ isBlockEventCancelled = true;
+ movingobjectposition = null;
+ }
}
+ // KigPaper end

// PaperSpigot start - Allow fishing hooks to fly through vanished players the shooter can't see
if (movingobjectposition != null && movingobjectposition.entity instanceof EntityPlayer && owner != null && owner instanceof EntityPlayer) {
@@ -215,12 +235,7 @@ public class EntityFishingHook extends Entity {
// KigPaper end
// PaperSpigot end

- // KigPaper start
- if (movingobjectposition != null && movingobjectposition.entity != null) {
- ProjectileCollideEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callProjectileCollideEvent(this, movingobjectposition.entity);
- if (event.isCancelled()) movingobjectposition = null;
- }
- // KigPaper end
+ // KigPaper - moved ProjectileCollideEvent call
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other than the same suggestions pointed out above, this looks good

vec3d1 = new Vec3D(movingobjectposition.pos.a, movingobjectposition.pos.b, movingobjectposition.pos.c);
}

+ ProjectilePreHitEvent event; // KigPaper
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

@posterhusky
Copy link
Author

made patches smaller and reverted checking block collision after entity is cancelled
changing 0187-Add-ProjectileCollideEvent.patch wont be necessary then

@posterhusky posterhusky requested a review from roccodev January 23, 2026 23:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants