Fix NCP in flight exemptions after arrow dismount

This commit is contained in:
NotAlexNoyle committed 2026-05-27 09:28:45 -04:00
1 parent 5a5ab5f029
commit ce0def7f8d
2 files changed
+85 -4

No files matched your search

@@ -13,8 +13,11 @@ import org.spigotmc.event.entity.EntityDismountEvent;
// Single cleanup point for the NoCheatPlus MOVING exemption taken in
// OnEntityShootBowEvent. EntityDismountEvent fires for every way a ride ends -- the
// arrow landing and being removed, the self-hit removal, manual dismount, or death --
// so restoring the checks here guarantees the exemption never leaks past the ride.
// arrow landing and being removed, the self-hit removal, manual dismount, or death.
// We do not restore checks immediately: the player still carries the arrow's momentum
// and coasts through the air, which NCP would flag as MOVING_CREATIVEFLY. NCPHook holds
// the exemption until the player has landed (bounded), so the exemption never leaks past
// the ride yet still covers the post-ride coast.
public class OnEntityDismountEvent implements Listener {
@EventHandler
@@ -32,7 +35,10 @@ public class OnEntityDismountEvent implements Listener {
if (arrow.getBasePotionData().getType() != PotionType.UNCRAFTABLE)
return;
NCPHook.unexemptMoving(player);
// Keep the exemption until the player has bled off the arrow's momentum and
// landed -- the post-ride coast is checked as a normal player move and would
// otherwise raise MOVING_CREATIVEFLY.
NCPHook.unexemptMovingWhenGrounded(player);
}
@@ -3,10 +3,14 @@
package net.trueog.flyingbowog;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitTask;
// Soft integration with NoCheatPlus. While a player rides a Flying Bow arrow they
// move through the air faster than survival movement allows. NCP would flag that as
@@ -42,6 +46,20 @@ public final class NCPHook {
private static boolean warnedNotApplied = false;
private static boolean warnedInvokeFailed = false;
// Pending "unexempt once grounded" pollers, keyed by player. One per player; a
// new
// ride cancels the previous poller so a late landing can never unexempt
// mid-flight.
private static final Map<UUID, BukkitTask> pendingUnexempt = new ConcurrentHashMap<>();
// Cap on how long the exemption is held after dismount while waiting for the
// player
// to land (server ticks). A fast launch coasts for a few seconds; this bounds
// the
// worst case so the exemption can never leak permanently if "on ground" never
// fires.
private static final int MAX_GROUNDING_TICKS = 200;
private NCPHook() {
}
@@ -52,6 +70,9 @@ public final class NCPHook {
if (!resolve())
return;
// A new ride supersedes any pending post-ride grounding poll.
cancelPendingUnexempt(player.getUniqueId());
try {
exemptMethod.invoke(null, player, movingCheckType);
@@ -81,7 +102,52 @@ public final class NCPHook {
}
// Restore MOVING checking once the ride ends.
// Called when the Flying Bow arrow ride ends (dismount). The player keeps the
// arrow's momentum after the arrow is gone and coasts through the air for many
// ticks; NCP checks those moves as the player -- not as a vehicle -- and raises
// MOVING_CREATIVEFLY. Dropping the exemption the instant the arrow dies leaves
// that
// coast unprotected. Instead, hold the exemption until the player is back on
// the
// ground (bounded by MAX_GROUNDING_TICKS), then restore checking.
public static void unexemptMovingWhenGrounded(Player player) {
if (!resolve())
return;
UUID id = player.getUniqueId();
cancelPendingUnexempt(id);
Plugin plugin = FlyingBowOG.getPlugin();
// Already grounded, or no scheduler available: restore immediately.
if (plugin == null || player.isOnGround()) {
unexemptMoving(player);
return;
}
final int[] ticks = { 0 };
BukkitTask task = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
Player online = Bukkit.getPlayer(id);
ticks[0]++;
if (online == null || !online.isOnline() || online.isOnGround() || ticks[0] >= MAX_GROUNDING_TICKS) {
cancelPendingUnexempt(id);
if (online != null)
unexemptMoving(online);
}
}, 1L, 1L);
pendingUnexempt.put(id, task);
}
// Restore MOVING checking now. Used internally once the player has landed.
public static void unexemptMoving(Player player) {
if (!resolve())
@@ -104,6 +170,15 @@ public final class NCPHook {
}
// Cancel and forget any pending grounding poller for a player.
private static void cancelPendingUnexempt(UUID id) {
BukkitTask task = pendingUnexempt.remove(id);
if (task != null)
task.cancel();
}
// Bind the NCP exemption API on first use. Returns whether the hook is usable.
private static synchronized boolean resolve() {