From ca9ba92a8d2b8f9a794f20cfcd81ebc52612c8b6 Mon Sep 17 00:00:00 2001 From: NotAlexNoyle Date: Tue, 16 Jun 2026 11:42:40 -0400 Subject: [PATCH] Don't count kills and deaths from duels matches --- .../battleplugins/tracker/BattleTracker.java | 2 + .../tracker/listener/PvEListener.java | 16 ++ .../tracker/listener/PvPListener.java | 11 ++ .../battleplugins/tracker/util/DuelsHook.java | 156 ++++++++++++++++++ src/main/resources/plugin.yml | 2 +- 5 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/battleplugins/tracker/util/DuelsHook.java diff --git a/src/main/java/org/battleplugins/tracker/BattleTracker.java b/src/main/java/org/battleplugins/tracker/BattleTracker.java index a38ef58..b89d749 100644 --- a/src/main/java/org/battleplugins/tracker/BattleTracker.java +++ b/src/main/java/org/battleplugins/tracker/BattleTracker.java @@ -87,6 +87,8 @@ public class BattleTracker extends JavaPlugin { public void onEnable() { this.shuttingDown = false; Bukkit.getPluginManager().registerEvents(new BattleTrackerListener(this), this); + // Captures duel match membership before Duels tears the match down on death. + Bukkit.getPluginManager().registerEvents(new org.battleplugins.tracker.util.DuelsHook.DeathGuard(), this); // Register default calculators this.registerCalculator(new EloCalculator(this.config.getRating().elo())); diff --git a/src/main/java/org/battleplugins/tracker/listener/PvEListener.java b/src/main/java/org/battleplugins/tracker/listener/PvEListener.java index f0bb1ff..3b31132 100644 --- a/src/main/java/org/battleplugins/tracker/listener/PvEListener.java +++ b/src/main/java/org/battleplugins/tracker/listener/PvEListener.java @@ -9,6 +9,7 @@ import org.battleplugins.tracker.feature.recap.Recap; import org.battleplugins.tracker.feature.recap.RecapEntry; import org.battleplugins.tracker.stat.Record; import org.battleplugins.tracker.stat.StatType; +import org.battleplugins.tracker.util.DuelsHook; import org.battleplugins.tracker.util.Util; import org.bukkit.entity.AnimalTamer; import org.bukkit.entity.Entity; @@ -45,6 +46,11 @@ public class PvEListener implements Listener { return; } + // Duel PvE/world deaths (fall, lava, void, mobs) must not count; use pre-captured marker (live query races Duels). + if (DuelsHook.isDuelDeath(killed)) { + return; + } + TrackedDataType dataType = TrackedDataType.PVE; EntityDamageEvent lastDamageCause = killed.getLastDamageCause(); @@ -124,6 +130,11 @@ public class PvEListener implements Listener { return; } + // A duelist killing a mob mid-match must not be tracked. + if (DuelsHook.isInMatch(killer)) { + return; + } + Record killerRecord = this.tracker.getOrCreateRecord(killer); if (killerRecord.isTracking()) { this.tracker.incrementValue(StatType.KILLS, killer); @@ -151,6 +162,11 @@ public class PvEListener implements Listener { return; } + // Don't build a damage recap from a Duels match (after cheap exits to keep reflection off the common path). + if (DuelsHook.isInMatch(player)) { + return; + } + if (!(event instanceof EntityDamageByEntityEvent entityEvent)) { if (!this.tracker.tracksData(TrackedDataType.WORLD)) { return; diff --git a/src/main/java/org/battleplugins/tracker/listener/PvPListener.java b/src/main/java/org/battleplugins/tracker/listener/PvPListener.java index b9049db..6544817 100644 --- a/src/main/java/org/battleplugins/tracker/listener/PvPListener.java +++ b/src/main/java/org/battleplugins/tracker/listener/PvPListener.java @@ -8,6 +8,7 @@ import org.battleplugins.tracker.feature.recap.RecapEntry; import org.battleplugins.tracker.stat.Record; import org.battleplugins.tracker.stat.StatType; import org.battleplugins.tracker.stat.TallyEntry; +import org.battleplugins.tracker.util.DuelsHook; import org.bukkit.entity.AnimalTamer; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; @@ -41,6 +42,11 @@ public class PvPListener implements Listener { return; } + // Duel deaths/kills must not be tracked; use the pre-captured marker (live query races Duels' own death handler). + if (DuelsHook.isDuelDeath(killed)) { + return; + } + EntityDamageEvent lastDamageCause = killed.getLastDamageCause(); Player killer = null; if (lastDamageCause instanceof EntityDamageByEntityEvent damageByEntityEvent) { @@ -118,6 +124,11 @@ public class PvPListener implements Listener { return; } + // Don't build a damage recap from a Duels match (after cheap exits to keep reflection off the common path). + if (DuelsHook.isInMatch(damaged)) { + return; + } + Entity sourceEntity = event.getDamager(); if (event.getDamager() instanceof Projectile proj) { if (proj.getShooter() instanceof Player shooter) { diff --git a/src/main/java/org/battleplugins/tracker/util/DuelsHook.java b/src/main/java/org/battleplugins/tracker/util/DuelsHook.java new file mode 100644 index 0000000..06eef89 --- /dev/null +++ b/src/main/java/org/battleplugins/tracker/util/DuelsHook.java @@ -0,0 +1,156 @@ +package org.battleplugins.tracker.util; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.PlayerDeathEvent; +import org.bukkit.plugin.Plugin; + +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; + +/** + * Soft, reflection-based integration with the Duels plugin (Duels-OG). + * + *

Kills and deaths that happen inside a Duels match must not be recorded by + * BattleTracker. This helper answers "is this player currently in a duel match?" + * without a compile-time dependency on Duels: if Duels is not installed, or its + * API ever changes, queries return {@code false} and tracking proceeds exactly + * as before.

+ * + *

Death-event ordering

+ *

Duels-OG handles {@link PlayerDeathEvent} at {@link EventPriority#HIGHEST} + * and synchronously marks the dead player as removed from the match, which makes + * a live {@link #isInMatch(Player)} query return {@code false} for the + * dead player by the time another {@code HIGHEST} listener runs. To avoid that + * race, {@link DeathGuard} records match membership at {@link EventPriority#LOWEST} + * (before Duels mutates the match) and the death listeners consult + * {@link #isDuelDeath(Player)} instead of querying live state.

+ * + *

Resolution targets Duels' public API ({@code me.realized.duels.api.Duels}) + * via {@code getArenaManager().isInMatch(Player)}. Both the renamed fork + * ("Duels-OG") and the upstream plugin ("Duels") are recognised.

+ */ +public final class DuelsHook { + + private static final String[] PLUGIN_NAMES = {"Duels-OG", "Duels"}; + + // Cache keyed to the Duels plugin instance so a reload/re-enable (new instance) forces re-resolution. + private static volatile Plugin cachedPlugin; + private static volatile java.lang.reflect.Method getArenaManager; + private static volatile java.lang.reflect.Method isInMatch; + + private static volatile boolean warned; + + // UUIDs whose in-progress death began in a duel; populated at LOWEST and cleared at MONITOR by DeathGuard. + private static final Set duelDeaths = ConcurrentHashMap.newKeySet(); + + private DuelsHook() { + } + + /** + * Records, before other plugins can mutate match state, whether each player + * death is happening inside a duel. Must be registered exactly once. + */ + public static final class DeathGuard implements Listener { + + @EventHandler(priority = EventPriority.LOWEST) + public void onPreDeath(PlayerDeathEvent event) { + if (isInMatch(event.getEntity())) { + duelDeaths.add(event.getEntity().getUniqueId()); + } + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onPostDeath(PlayerDeathEvent event) { + duelDeaths.remove(event.getEntity().getUniqueId()); + } + } + + /** + * Whether the player's in-progress death began inside a duel match. Valid + * only while a {@link PlayerDeathEvent} for that player is being processed; + * use this (not {@link #isInMatch(Player)}) inside death handlers. + * + * @param player the dying player (may be null) + * @return {@code true} if the death should be excluded from tracking + */ + public static boolean isDuelDeath(Player player) { + return player != null && duelDeaths.contains(player.getUniqueId()); + } + + /** + * Returns whether the given player is, right now, in an active Duels match. + * + *

Safe for live use outside death handling (e.g. damage recap, mob kills), + * where match state is not being torn down. For {@link PlayerDeathEvent} + * handlers use {@link #isDuelDeath(Player)} instead.

+ * + * @param player the player to check (may be null) + * @return {@code true} only if Duels is installed, enabled, and reports the + * player as being in a match; {@code false} otherwise + */ + public static boolean isInMatch(Player player) { + if (player == null) { + return false; + } + + Plugin duels = getDuels(); + if (duels == null) { + return false; + } + + try { + resolve(duels); + + if (getArenaManager == null || isInMatch == null) { + return false; + } + + Object arenaManager = getArenaManager.invoke(duels); + if (arenaManager == null) { + return false; + } + + return (boolean) isInMatch.invoke(arenaManager, player); + } catch (Throwable t) { + // Never let a reflection/API problem break tracking, but surface it once for diagnosis. + if (!warned) { + warned = true; + Bukkit.getLogger().log(Level.WARNING, + "[BattleTracker] Failed to query Duels match state; duel kills/deaths " + + "may be tracked until this is resolved.", t); + } + return false; + } + } + + private static Plugin getDuels() { + for (String name : PLUGIN_NAMES) { + Plugin plugin = Bukkit.getPluginManager().getPlugin(name); + if (plugin != null && plugin.isEnabled()) { + return plugin; + } + } + + return null; + } + + private static void resolve(Plugin duels) throws ReflectiveOperationException { + if (duels == cachedPlugin && getArenaManager != null && isInMatch != null) { + return; + } + + java.lang.reflect.Method arenaManagerMethod = duels.getClass().getMethod("getArenaManager"); + Object arenaManager = arenaManagerMethod.invoke(duels); + java.lang.reflect.Method inMatchMethod = arenaManager.getClass().getMethod("isInMatch", Player.class); + + getArenaManager = arenaManagerMethod; + isInMatch = inMatchMethod; + cachedPlugin = duels; + } +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 390414a..de2e7da 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -5,7 +5,7 @@ description: A standalone plugin that tracks PVP & PVE statistics, provides cust author: BattlePlugins website: https://battleplugins.org api-version: 1.19 -softdepend: [BattleArena] +softdepend: [BattleArena, Duels-OG, Duels] commands: battletracker: description: The main BattleTracker command.