fix: close two unbounded map leaks that survived player quit

Обе карты росли без границы и никогда не освобождались.

1. MagicListener.lastCast / lastCastTime
   Писались на каждом применении магии (recordCast) и не удалялись НИКОГДА —
   ни на выходе, ни на смерти. Каждый игрок, хоть раз применивший магию,
   навсегда оставлял две записи. Данные копеечные, но карты растут без
   границы весь срок жизни сервера. Очистка вынесена в общий forget(UUID),
   который зовут и onQuit, и onDeath.

2. TensionManager.battleEnemies
   На выходе не удалялся. Расчёт был на то, что запись исчезнет сама через
   BATTLE_EXPIRE_MS, но эта проверка требует непустого lastCombat — а он
   удалялся в той же строке выше. В итоге запись жила вечно, если в бою был
   противник, который не умирает: босс или запертый моб на ферме. Множество
   не пустеет, lastCombat == null, условие истечения не проверяется, и
   tickBattle обходит эту запись каждый тик до конца жизни сервера.

   Дополнительно tickBattle теперь снимает боевую запись и при lastCombat ==
   null, а не только по таймауту. Это закрывает не только выход, но и любую
   другую рассинхронизацию: без боя и без отметки времени бой не бой.
This commit is contained in:
SashegDev
2026-10-04 13:39:09 +00:00
parent 10894eab37
commit 82bb585f88
2 changed files with 31 additions and 7 deletions
@@ -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());
}