Disable no knockback for netherite in PvP combat

This commit is contained in:
NotAlexNoyle committed 2026-06-24 16:29:26 -04:00
1 parent 35bbe9ecba
commit 2a34066187
6 files changed
+264 -25

No files matched your search

@@ -137,8 +137,12 @@ public class BukkitBase extends Base {
protected void registerPlatformListeners() {
registerPluginListeners(
new BukkitPlayerDamageListener(),
new BukkitPlayerKnockbackListener()
new BukkitPlayerKnockbackListener(),
new me.caseload.knockbacksync.listener.bukkit.KnockbackResistanceNullifier()
);
// Reconcile away any leftover owned knockback-resistance marker for players already
// online (e.g. across a /reload), so none is stranded.
me.caseload.knockbacksync.listener.bukkit.KnockbackResistanceNullifier.cleanupAll();
}
@Override
@@ -20,6 +20,9 @@ public final class BukkitLoaderPlugin extends JavaPlugin {
@Override
public void onDisable() {
// Reconcile away any leftover owned knockback-resistance marker so no player is left
// with reduced resistance after the plugin unloads.
me.caseload.knockbacksync.listener.bukkit.KnockbackResistanceNullifier.cleanupAll();
// Drop listener handles before any lifecycle decision; the shared API may outlive this plugin.
core.unregisterPacketListeners();
// Terminate only when this plugin owns the PacketEvents lifecycle (shaded build).
@@ -2,10 +2,9 @@ package me.caseload.knockbacksync.listener.bukkit;
import me.caseload.knockbacksync.Base;
import me.caseload.knockbacksync.listener.PlayerDamageListener;
import me.caseload.knockbacksync.manager.ConfigManager;
import me.caseload.knockbacksync.player.BukkitPlayer;
import me.caseload.knockbacksync.util.MultiLibUtil;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeInstance;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@@ -15,8 +14,44 @@ import org.bukkit.event.entity.EntityDamageByEntityEvent;
public class BukkitPlayerDamageListener extends PlayerDamageListener implements Listener {
private static boolean knockbackResistanceStripUnsupported = false;
/**
* D3 — projectile PvP knockback-resistance nullification.
*
* <p>For a hit that {@link PvpSource} classifies as {@code PROJECTILE} PvP, zero the
* victim's effective knockback resistance BEFORE vanilla computes the knockback, so a
* netherite victim takes full (diamond-level) projectile knockback. Runs at {@code LOW}
* so it precedes OCM's {@code projectile-knockback} damage raise ({@code NORMAL}) and
* vanilla knockback. The decision is gated on classification + config only — never on the
* current event damage, because OCM may not have raised snowball/egg/ender-pearl damage
* yet at this priority.</p>
*/
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
public void onProjectilePvpDamage(EntityDamageByEntityEvent event) {
final ConfigManager config = Base.INSTANCE.getConfigManager();
// Single source of truth: active only when toggled on, legacy knockback enabled,
// and knockback resistance is NOT being kept (matching OCM's default).
if (!config.isToggled() || !config.isLegacyKnockbackEnabled() || config.isLegacyKnockbackResistanceEnabled())
return;
if (PvpSource.classify(event) != PvpSource.Type.PROJECTILE)
return;
final Player victim = (Player) event.getEntity();
if (MultiLibUtil.isExternalPlayer(victim))
return;
KnockbackResistanceNullifier.nullify(victim);
}
/**
* Melee PvP: KnockbackSync owns the 1.8 melee velocity via {@code LegacyKnockback}
* (applied at {@code PlayerVelocityEvent}), so melee already delivers full knockback
* without touching the resistance attribute. The previous {@code stripKnockbackResistance()}
* (which removed the victim's {@code KNOCKBACK_RESISTANCE} modifiers and never restored them,
* leaking into PvE) has been deleted. Projectile/explosion/fishing hits are NOT routed
* through this melee path (their damager is not a player), so no stale legacy-knockback
* vector can be stored for them.
*/
@EventHandler(ignoreCancelled = true)
public void onPlayerDamage(EntityDamageByEntityEvent event) {
Entity victim = event.getEntity();
@@ -27,27 +62,6 @@ public class BukkitPlayerDamageListener extends PlayerDamageListener implements
if (MultiLibUtil.isExternalPlayer((Player) victim))
return;
// Legacy (1.8) parity: with knockback resistance disabled (OCM default), nullify the
// victim's knockback resistance so full 1.8 knockback applies and the velocity event
// always fires. Must run before vanilla computes knockback (default priority is fine).
if (Base.INSTANCE.getConfigManager().isLegacyKnockbackEnabled()
&& !Base.INSTANCE.getConfigManager().isLegacyKnockbackResistanceEnabled()) {
stripKnockbackResistance((Player) victim);
}
onPlayerDamage(new BukkitPlayer((Player) victim), new BukkitPlayer((Player) attacker));
}
private void stripKnockbackResistance(Player victim) {
if (knockbackResistanceStripUnsupported) return;
try {
AttributeInstance attribute = victim.getAttribute(Attribute.GENERIC_KNOCKBACK_RESISTANCE);
if (attribute == null) return;
attribute.getModifiers().forEach(attribute::removeModifier);
} catch (Throwable t) {
// Attribute enum/constant differs on this server version; disable to avoid log spam.
knockbackResistanceStripUnsupported = true;
Base.INSTANCE.getLogger().warning("Could not nullify knockback resistance for legacy knockback: " + t.getMessage());
}
}
}
@@ -0,0 +1,121 @@
package me.caseload.knockbacksync.listener.bukkit;
import me.caseload.knockbacksync.Base;
import org.bukkit.Bukkit;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeInstance;
import org.bukkit.attribute.AttributeModifier;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import java.util.UUID;
/**
* D3 — Resistance Nullification (M1).
*
* <p>For player-vs-player projectile hits (classified by {@link PvpSource}), forces the
* victim's effective {@code generic.knockback_resistance} to 0 for the duration of the
* hit by adding a single plugin-owned {@code MULTIPLY_SCALAR_1} {@code -1.0} modifier,
* lets vanilla compute full knockback, then removes the owned marker on the next tick.
* Equipment and other plugins' modifiers are never read, captured, or removed — only the
* owned marker (identified by a constant UUID) is added/removed.</p>
*
* <p>Melee PvP is owned by {@code LegacyKnockback} (which already ignores resistance when
* {@code enable_knockback_resistance=false}) and needs no attribute work; fishing PvP is
* owned by OCM's {@code old-fishing-knockback}. Explosions are out of scope on 1.19.4.</p>
*
* <p>The marker is permanent NMS state until removed, so a leftover marker is reconciled
* away on join/quit and on plugin enable/disable (see {@link #cleanupAll()}), guaranteeing
* a player is never left permanently with reduced resistance.</p>
*/
public final class KnockbackResistanceNullifier implements Listener {
// Constant, plugin-owned modifier identity. Add skips if this UUID is already present;
// removal matches by this UUID. Equipment/foreign modifiers are never touched.
private static final UUID MODIFIER_UUID = UUID.fromString("7b2a1c84-4d3e-4f0a-9c6b-1e5a9d8c7f30");
private static final String MODIFIER_NAME = "knockbacksync_pvp_kb_nullify";
// Mirrors the previous stripKnockbackResistance guard: if the attribute is unavailable
// on this server version, disable for the session and warn once (no per-hit log spam).
private static volatile boolean unsupported = false;
/**
* Force the victim's effective knockback resistance to 0 for the current hit by adding the
* owned marker, then schedule its removal on the next tick. Idempotent and re-entrancy safe:
* if the marker is already present (e.g. a second hit in the same tick) the add is skipped.
*
* <p>Must be called before vanilla computes the hit's knockback (i.e. during the damage
* event at an early priority).</p>
*/
public static void nullify(Player victim) {
final AttributeInstance attribute = resolve(victim);
if (attribute == null) return;
if (hasOwnedModifier(attribute)) return;
try {
attribute.addModifier(new AttributeModifier(
MODIFIER_UUID, MODIFIER_NAME, -1.0, AttributeModifier.Operation.MULTIPLY_SCALAR_1));
} catch (IllegalArgumentException alreadyApplied) {
return; // raced — modifier already present; nothing to add
}
// Remove on the NEXT tick. Same-event (MONITOR/finally) removal is forbidden: vanilla
// computes the knockback after the event returns, so an in-event removal would restore
// the attribute before vanilla reads it. On the non-Folia target this is a main-thread
// next-tick task; it stays compile-safe (no Player#getScheduler()).
Base.INSTANCE.getScheduler().runTaskLater(() -> remove(victim), 1L);
}
/** Remove the owned marker if present. Idempotent; never touches foreign/equipment modifiers. */
public static void remove(Player victim) {
final AttributeInstance attribute = resolve(victim);
if (attribute == null) return;
for (AttributeModifier modifier : attribute.getModifiers()) {
if (MODIFIER_UUID.equals(modifier.getUniqueId())) {
attribute.removeModifier(modifier);
}
}
}
/** Strip the owned marker from every online player (plugin enable/disable reconciliation). */
public static void cleanupAll() {
for (Player player : Bukkit.getOnlinePlayers()) {
remove(player);
}
}
private static boolean hasOwnedModifier(AttributeInstance attribute) {
for (AttributeModifier modifier : attribute.getModifiers()) {
if (MODIFIER_UUID.equals(modifier.getUniqueId())) return true;
}
return false;
}
private static AttributeInstance resolve(Player victim) {
if (unsupported || victim == null) return null;
try {
return victim.getAttribute(Attribute.GENERIC_KNOCKBACK_RESISTANCE);
} catch (Throwable t) {
unsupported = true;
Base.INSTANCE.getLogger().warning(
"Could not access KNOCKBACK_RESISTANCE attribute; PvP knockback-resistance "
+ "nullification disabled for this session: " + t.getMessage());
return null;
}
}
// ── R7 fail-safe: reconcile away any leftover owned marker ──────────────────────────────
@EventHandler
public void onJoin(PlayerJoinEvent event) {
remove(event.getPlayer());
}
@EventHandler
public void onQuit(PlayerQuitEvent event) {
remove(event.getPlayer());
}
}
@@ -0,0 +1,85 @@
package me.caseload.knockbacksync.listener.bukkit;
import com.github.retrooper.packetevents.protocol.player.User;
import me.caseload.knockbacksync.manager.PlayerDataManager;
import me.caseload.knockbacksync.player.BukkitPlayer;
import me.caseload.knockbacksync.player.PlayerData;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.entity.Projectile;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.projectiles.ProjectileSource;
/**
* D1 — PvP Source Attribution.
*
* <p>Classifies a damage event on a player victim into a source class so the
* knockback-resistance nullifier (D3) knows whether and how to act. Attribution is
* self-inclusive: the victim's own projectile (e.g. their own arrow) still counts as
* PvP. This classifier only answers "is this player-attributed, and by what source
* class?" — it does not modify damage or knockback.</p>
*
* <p>Scope note (Purpur 1.19.4 target): netherite {@code generic.knockback_resistance}
* only reduces knockback applied through {@code LivingEntity.knockback(...)} (melee and
* projectile). Explosion knockback is not reduced by it on 1.19.4 (Blast Protection
* governs that), and wind charges do not exist, so explosion/wind-charge sources are
* classified {@link Type#NONE}.</p>
*/
public final class PvpSource {
public enum Type {
/** Direct player melee. Owned by {@code LegacyKnockback}; no attribute nullification. */
MELEE,
/** Player-shot projectile (arrow, spectral arrow, trident, snowball, egg, ender pearl, ...). Nullified by D3. */
PROJECTILE,
/** Fishing-rod hit (OCM {@code old-fishing-knockback}); owned by the rod sync. No nullification. */
FISHING,
/** Not attributable to a player (mob, environment, explosion, ...). */
NONE
}
private PvpSource() {
}
/**
* @param event a damage-by-entity event (the caller has not necessarily checked the victim type)
* @return the PvP source class; {@link Type#NONE} when the damage is not attributable to a player
*/
public static Type classify(EntityDamageByEntityEvent event) {
if (!(event.getEntity() instanceof Player)) return Type.NONE;
final Player victim = (Player) event.getEntity();
final Entity damager = event.getDamager();
// Direct player damager: either a melee hit, or an OCM fishing-rod hit.
// old-fishing-knockback deals its damage via damage(victim, rodder), so the
// damager is the rodding player (not the bobber). Distinguish the rod hit via
// the rod-pending marker KnockbackSync stores for the rod path. Both MELEE and
// FISHING are owned elsewhere and get no attribute nullification, so a mislabel
// between the two cannot change knockback behaviour.
if (damager instanceof Player) {
return isRodPending(victim) ? Type.FISHING : Type.MELEE;
}
// Player-shot projectile, resolved generically via ProjectileSource so any
// player-shot projectile type is covered without a per-type list (including
// types absent from the compile-time API). Self-inclusive: the shooter may be
// the victim. A non-player projectile source (e.g. a dispenser's
// BlockProjectileSource) or a null shooter is not PvP. Note: the ender pearl's
// post-teleport fall damage is a separate non-by-entity event, so it never
// reaches this projectile branch.
if (damager instanceof Projectile) {
final ProjectileSource shooter = ((Projectile) damager).getShooter();
return (shooter instanceof Player) ? Type.PROJECTILE : Type.NONE;
}
// Explosions (TNT, end crystal, fireball), mobs, environment: out of scope on 1.19.4.
return Type.NONE;
}
private static boolean isRodPending(Player victim) {
final User user = new BukkitPlayer(victim).getUser();
if (user == null) return false;
final PlayerData data = PlayerDataManager.getPlayerData(user);
return data != null && data.isRodKnockbackPending();
}
}
+12
View File
@@ -65,6 +65,18 @@ legacy_knockback:
# When false (default, matching OCM), netherite/armour knockback resistance is
# ignored so the velocity event always fires and full 1.8 knockback is applied.
# When true, horizontal knockback is reduced by the victim's resistance attribute.
#
# Scope of the false setting:
# - Melee PvP: LegacyKnockback already ignores resistance (it owns the velocity).
# - Projectile PvP (bow/arrow, trident, and snowball/egg/ender pearl once
# OldCombatMechanics' projectile-knockback gives them damage): KnockbackSync
# temporarily nullifies the victim's knockback-resistance attribute for the hit
# so vanilla applies full knockback, then restores it (PvE is unaffected).
# - Fishing-rod PvP: owned by OCM old-fishing-knockback + the rod API (already 1.8).
# - Explosions are NOT affected by knockback resistance on 1.19.4, so they are
# out of scope here.
# Ownership: KnockbackSync owns player knockback; keep OCM's old-player-knockback
# module DISABLED (see the IMPORTANT note above).
enable_knockback_resistance: false
# This is the rate the server will use for (amongst other things) sending packets about entities to players