From 82bb585f88ae124c72275d3162f4f1c95582dfe7 Mon Sep 17 00:00:00 2001 From: SashegDev Date: Sun, 4 Oct 2026 13:39:09 +0000 Subject: [PATCH] fix: close two unbounded map leaks that survived player quit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Обе карты росли без границы и никогда не освобождались. 1. MagicListener.lastCast / lastCastTime Писались на каждом применении магии (recordCast) и не удалялись НИКОГДА — ни на выходе, ни на смерти. Каждый игрок, хоть раз применивший магию, навсегда оставлял две записи. Данные копеечные, но карты растут без границы весь срок жизни сервера. Очистка вынесена в общий forget(UUID), который зовут и onQuit, и onDeath. 2. TensionManager.battleEnemies На выходе не удалялся. Расчёт был на то, что запись исчезнет сама через BATTLE_EXPIRE_MS, но эта проверка требует непустого lastCombat — а он удалялся в той же строке выше. В итоге запись жила вечно, если в бою был противник, который не умирает: босс или запертый моб на ферме. Множество не пустеет, lastCombat == null, условие истечения не проверяется, и tickBattle обходит эту запись каждый тик до конца жизни сервера. Дополнительно tickBattle теперь снимает боевую запись и при lastCombat == null, а не только по таймауту. Это закрывает не только выход, но и любую другую рассинхронизацию: без боя и без отметки времени бой не бой. --- .../sashegdev/blackknife/MagicListener.java | 24 ++++++++++++++----- .../sashegdev/blackknife/TensionManager.java | 14 ++++++++++- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/main/java/me/sashegdev/blackknife/MagicListener.java b/src/main/java/me/sashegdev/blackknife/MagicListener.java index 124d3b6..f0bdea6 100644 --- a/src/main/java/me/sashegdev/blackknife/MagicListener.java +++ b/src/main/java/me/sashegdev/blackknife/MagicListener.java @@ -225,17 +225,29 @@ public class MagicListener implements Listener { return dmg * 0.7; } + /** + * Забыть всё состояние игрока. Вызывается и на выходе, и на смерти. + * + *

Про lastCast: эти две карты писались на каждом применении магии и + * НИКОГДА не удалялись. Каждый игрок, хоть раз применивший магию, навсегда + * оставлял две записи — на весь срок жизни сервера. Данные там копеечные, + * но карты растут без границы, и их приходится обходить каждый тик. + */ + private void forget(UUID id) { + var task = shieldTasks.remove(id); + if (task != null) task.cancel(); + shieldEnd.remove(id); + lastCast.remove(id); + lastCastTime.remove(id); + } + @EventHandler public void onQuit(org.bukkit.event.player.PlayerQuitEvent e) { - var task = shieldTasks.remove(e.getPlayer().getUniqueId()); - if (task != null) task.cancel(); - shieldEnd.remove(e.getPlayer().getUniqueId()); + forget(e.getPlayer().getUniqueId()); } @EventHandler public void onDeath(org.bukkit.event.entity.PlayerDeathEvent e) { - var task = shieldTasks.remove(e.getEntity().getUniqueId()); - if (task != null) task.cancel(); - shieldEnd.remove(e.getEntity().getUniqueId()); + forget(e.getEntity().getUniqueId()); } } diff --git a/src/main/java/me/sashegdev/blackknife/TensionManager.java b/src/main/java/me/sashegdev/blackknife/TensionManager.java index 22ca05d..b8ddd3c 100644 --- a/src/main/java/me/sashegdev/blackknife/TensionManager.java +++ b/src/main/java/me/sashegdev/blackknife/TensionManager.java @@ -212,7 +212,11 @@ public class TensionManager implements Listener { || (ent instanceof org.bukkit.entity.LivingEntity le && le.isDead()); }); final Long last = lastCombat.get(pid); - if (set.isEmpty() || (last != null && now - last > BATTLE_EXPIRE_MS)) { + // lastCombat == null означает, что бой не начинался или уже вышел из + // состояния. Множество при этом может быть непустым (противник жив), + // и тогда условие истечения по времени не проверяется вовсе — запись + // остаётся навсегда. Без боя и без отметки времени бой не бой. + if (last == null || set.isEmpty() || (now - last > BATTLE_EXPIRE_MS)) { if (drop == null) { drop = new java.util.ArrayList<>(4); } @@ -281,6 +285,14 @@ public class TensionManager implements Listener { tp.remove(id); lastCombat.remove(id); lastPct.remove(id); + // Утечка: battleEnemies на выходе не чистился. Расчёт на то, что запись + // исчезнет сама через BATTLE_EXPIRE_MS, но этот путь требует непустого + // lastCombat — а его мы только что удалили строкой выше. В итоге запись + // могла жить вечно: пока в бою есть противник, который не умирает + // (босс на ферме, запертый моб), множество не пустеет, lastCombat равен + // null, и условие истечения не проверяется. Дальше tickBattle обходит + // эту запись каждый тик до конца жизни сервера. + battleEnemies.remove(id); AbilityCooldown.clearAll(e.getPlayer()); }