Don't count kills and deaths from duels matches

This commit is contained in:
NotAlexNoyle committed 2026-06-16 11:42:40 -04:00
1 parent dd7814f337
commit ca9ba92a8d
5 files changed
+186 -1

No files matched your search

@@ -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()));
@@ -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;
@@ -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) {
@@ -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).
*
* <p>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.</p>
*
* <h2>Death-event ordering</h2>
* <p>Duels-OG handles {@link PlayerDeathEvent} at {@link EventPriority#HIGHEST}
* and synchronously marks the dead player as removed from the match, which makes
* a <em>live</em> {@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.</p>
*
* <p>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.</p>
*/
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<UUID> 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.
*
* <p>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.</p>
*
* @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;
}
}
+1 -1
View File
@@ -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.