Disable no knockback for netherite in PvP combat
This commit is contained in:
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).
|
||||
|
||||
+38
-24
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+121
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user