Compatibility with LightningGrim

This commit is contained in:
NotAlexNoyle committed 2026-06-16 11:19:43 -04:00
1 parent ce0def7f8d
commit 9c3be631ff
4 files changed
+262 -1

No files matched your search

@@ -0,0 +1,255 @@
// This is free and unencumbered software released into the public domain.
// Authors: NotAlexNoyle.
package net.trueog.flyingbowog;
import java.lang.reflect.Field;
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 LightningGrim (Grim anticheat), the Grim-side twin of NCPHook.
// While a player rides a Flying Bow arrow -- and during the momentum coast after the
// arrow is gone -- they move faster than survival physics allows. Grim's prediction
// engine would treat that as illegal movement, so we exempt the player for the duration.
//
// Grim has no per-group exempt API, and its public API (AbstractCheck#setEnabled) only
// gates packet modification, not flagging/setback (Check#flag short-circuits on
// player.disableGrim, not isEnabled). The one lever that fully exempts -- skipping
// prediction, flags and setbacks alike -- is the GrimPlayer#disableGrim field; Grim
// declares it once and never reassigns it, so it is a clean external toggle.
//
// We reach it without a compile-time dependency: Grim publishes only its api package
// (no GrimPlayer impl) and disableGrim is an impl field, so reflection is required
// regardless. The GrimAbstractAPI instance is fetched name-agnostically from Bukkit's
// ServicesManager (Grim registers it there); getGrimUser(UUID) returns the GrimPlayer,
// whose public disableGrim field we flip on its netty thread via GrimUser#runSafely.
// If Grim is absent the hook is a silent no-op; if it is present but the API will not
// bind we log once, because that is the only way a ride can still be flagged.
public final class GrimHook {
// Cached reflection handles, resolved once on first use. ready == true means
// the
// API bound and exemption calls will be attempted.
private static boolean resolved = false;
private static boolean ready = false;
private static Class<?> apiClass;
private static Method getGrimUserMethod;
private static Method runSafelyMethod;
// Resolved lazily from the first GrimUser instance and cached (impl class
// field).
private static Field disableGrimField;
// One-shot guards so a persistent runtime fault logs once, not once per shot.
private static boolean warnedNoUser = false;
private static boolean warnedInvokeFailed = false;
// Pending "unexempt once grounded" pollers, keyed by 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); bounds the worst case so it can never leak
// permanently.
private static final int MAX_GROUNDING_TICKS = 200;
private GrimHook() {
}
// Exempt the player from all Grim checks while riding a Flying Bow arrow.
public static void exempt(Player player) {
if (!resolve())
return;
// A new ride supersedes any pending post-ride grounding poll.
cancelPendingUnexempt(player.getUniqueId());
setDisableGrim(player, true);
}
// Called when the ride ends (dismount). The player keeps the arrow's momentum
// and
// coasts through the air, which Grim still checks as a normal player move, so
// we
// hold the exemption until the player has landed (bounded by
// MAX_GROUNDING_TICKS).
public static void unexemptWhenGrounded(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()) {
setDisableGrim(player, false);
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)
setDisableGrim(online, false);
}
}, 1L, 1L);
pendingUnexempt.put(id, task);
}
// Flip GrimPlayer#disableGrim for the player on Grim's netty thread
// (runSafely).
private static void setDisableGrim(Player player, boolean value) {
if (!resolve())
return;
try {
Object api = Bukkit.getServicesManager().load(apiClass);
// Service not registered yet (Grim still starting): nothing to toggle.
if (api == null)
return;
Object user = getGrimUserMethod.invoke(api, player.getUniqueId());
if (user == null) {
if (!warnedNoUser) {
warnedNoUser = true;
log(Level.WARNING, "LightningGrim has no tracked user for " + player.getName()
+ "; the Flying Bow ride may be flagged as illegal movement.");
}
return;
}
final Object grimUser = user;
// disableGrim is an impl field; resolve from the live object once and cache.
if (disableGrimField == null)
disableGrimField = grimUser.getClass().getField("disableGrim");
// Run on the player's netty thread so the write is ordered with Grim's reads.
runSafelyMethod.invoke(grimUser, (Runnable) () -> {
try {
disableGrimField.setBoolean(grimUser, value);
} catch (Throwable t) {
if (!warnedInvokeFailed) {
warnedInvokeFailed = true;
log(Level.WARNING,
"Failed to set LightningGrim disableGrim for " + player.getName() + ": " + t);
}
}
});
} catch (Throwable t) {
if (!warnedInvokeFailed) {
warnedInvokeFailed = true;
log(Level.WARNING, "Failed to toggle LightningGrim exemption for " + player.getName() + ": " + t);
}
}
}
// 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 Grim API on first use. Returns whether the hook is usable.
private static synchronized boolean resolve() {
if (resolved)
return ready;
resolved = true;
// Grim not installed: stay silent, the hook is simply inactive.
if (Bukkit.getPluginManager().getPlugin("LightningGrim") == null)
return false;
try {
apiClass = Class.forName("ac.grim.grimac.api.GrimAbstractAPI");
Class<?> userClass = Class.forName("ac.grim.grimac.api.GrimUser");
// getGrimUser(UUID) is the non-deprecated lookup; runSafely runs our write
// on the player's netty thread.
getGrimUserMethod = apiClass.getMethod("getGrimUser", UUID.class);
runSafelyMethod = userClass.getMethod("runSafely", Runnable.class);
ready = true;
log(Level.INFO, "Hooked into LightningGrim; Flying Bow rides are exempted from Grim checks.");
} catch (Throwable t) {
// Grim present but its API would not bind -- this is the failure mode that
// makes exemptions silently no-op, so it must be visible.
ready = false;
log(Level.WARNING, "LightningGrim is installed but its API could not be bound (" + t
+ "); Flying Bow rides will be flagged. Check that the running LightningGrim version exposes"
+ " ac.grim.grimac.api.GrimAbstractAPI via the Bukkit services manager.");
}
return ready;
}
// Route log lines through the plugin logger, falling back to the server logger.
private static void log(Level level, String message) {
Plugin plugin = FlyingBowOG.getPlugin();
if (plugin != null) {
plugin.getLogger().log(level, message);
} else {
Bukkit.getLogger().log(level, "[FlyingBow-OG] " + message);
}
}
}
@@ -2,6 +2,7 @@
// Authors: NotAlexNoyle.
package net.trueog.flyingbowog.Listeners;
import net.trueog.flyingbowog.GrimHook;
import net.trueog.flyingbowog.NCPHook;
import org.bukkit.Color;
import org.bukkit.entity.Arrow;
@@ -39,6 +40,7 @@ public class OnEntityDismountEvent implements Listener {
// landed -- the post-ride coast is checked as a normal player move and would
// otherwise raise MOVING_CREATIVEFLY.
NCPHook.unexemptMovingWhenGrounded(player);
GrimHook.unexemptWhenGrounded(player);
}
@@ -5,6 +5,7 @@ package net.trueog.flyingbowog.Listeners;
import com.destroystokyo.paper.event.player.PlayerArmorChangeEvent; // Purpur 1.19
import java.util.Map;
import net.trueog.flyingbowog.FlyingBowOG;
import net.trueog.flyingbowog.GrimHook;
import net.trueog.flyingbowog.NCPHook;
import net.trueog.utilitiesog.UtilitiesOG;
import org.bukkit.Bukkit;
@@ -82,6 +83,9 @@ public class OnEntityShootBowEvent implements Listener {
// Cleared on dismount in OnEntityDismountEvent.
NCPHook.exemptMoving(shooter);
// Same for LightningGrim (Grim anticheat); cleared on dismount.
GrimHook.exempt(shooter);
}, 1L);
// Strip Infinity from the bow (too OP for this item).
+1 -1
View File
@@ -4,7 +4,7 @@ main: net.trueog.flyingbowog.FlyingBowOG
authors: [barny1094875, NotAlexNoyle]
api-version: 1.19
depend: [Item-NBT-API]
softdepend: [NoCheatPlus]
softdepend: [NoCheatPlus, LightningGrim]
commands:
flyingbow:
usage: /flyingbow