perf: batch particles, drop LUNGE, fix target search, stop per-tick map copy
Measured on zern.cc with 20 mineflayer bots on SNIPE, empty world and then with 625 entities clustered around them. getLungeTarget searched a 120x120x120 cube to find one entity inside a 5.7 degree cone, and ran TWO raytraces per entity inside that loop. The second one, rayTraceBlocks(eye, dir, 120), does not depend on the entity at all: eye, dir and the distance are loop-invariant, so the same answer was recomputed once per entity, each time walking up to 120 blocks. On a populated world that is up to 2N raytraces per call, and updatePreview calls it every 4 ticks per player holding a knife. Now the query is an AABB along the look direction, the block ray is computed once before the loop, and hasLineOfSight is only called on candidates in distance order - usually exactly once. The common case of zero or one target allocates nothing at all; tracking the two nearest inline avoids the lists and the sort entirely. Honest note on the measurement: this fix did NOT show up. 10.5 ms median before vs 10.8 ms after with 625 entities nearby. Working the numbers back, that path costs roughly 1% of capacity in this scenario, which is below the noise floor. The code change is correct by inspection, not by measurement, and it would matter far more in a world with thousands of entities than 625. LUNGE is removed. Its shockwave drew points = radius*6 and issued THREE separate spawnParticle calls per point, so a radius-10 ring sent 180 packets PER TICK, expanding, per cast. That is gone with the ability. Particle batching, which is the real cost in this plugin. Every spawnParticle is one packet per viewer. MagicListener's hit ring was 36 separate calls and its explosion shell was 72; the OVERHEAD aiming ring was 32 packets every tick. spawnParticle with count>1 spreads particles inside the offset volume, so a ring of radius r becomes a filled disc of the same radius - indistinguishable at half-block scale, and the particle count is unchanged. New Particles helper. TensionManager.updateBar ran every tick per player: String.format, two Component.text, and a progress packet. Now it repaints only when the whole percent changes, and the percent is computed with integer math. At 0.25/tick regen that is four repaints a second instead of twenty, and zero on a full bar. The percent cache is cleared on quit and resynced on death so the bar cannot get stuck. tickBattle copied the entire battleEnemies map, including every nested set, twenty times a second - purely so it could iterate and delete from the same map. Keys are now collected into a list and dropped after the loop. getMode now collapses an unknown stored mode onto STARS. Without that a knife issued before LUNGE was removed could not be mode-switched at all: the index lookup found nothing, stayed at zero, and the knife was stuck.
This commit is contained in:
@@ -23,8 +23,11 @@ public class TensionManager implements Listener {
|
||||
private final Map<UUID, Long> lastDodge = new HashMap<>();
|
||||
private final Map<UUID, Boolean> ownerCache = new HashMap<>();
|
||||
private final Map<UUID, Long> ownerCacheTime = new HashMap<>();
|
||||
private final Map<UUID, Integer> lastPct = new HashMap<>();
|
||||
private static final long OWNER_CACHE_TTL = 2000L;
|
||||
public static final float MAX_TP = 250f;
|
||||
/** Бой считается кончившимся после этого простоя без TP-активности. */
|
||||
private static final long BATTLE_EXPIRE_MS = 180_000L;
|
||||
|
||||
private float passiveRegenPerTick = 0.25f;
|
||||
private long passiveRegenDelayMs = 3000L;
|
||||
@@ -116,25 +119,39 @@ public class TensionManager implements Listener {
|
||||
return mgr != null && mgr.hasAwakened(p.getUniqueId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Перерисовка полосы. Вызывается каждый тик на каждого игрока, поэтому
|
||||
* работает по дешёвому пути: пакет прогресса и новый Component уходят только
|
||||
* когда изменился целый процент. При регене 0.25/тик это 4 перерисовки в
|
||||
* секунду вместо 20, а на полной полосе — ноль.
|
||||
*
|
||||
* <p>Проценты считаются целочисленно: {@code String.format} на каждом тике
|
||||
* на каждого игрока это сотни вызовов в секунду ради строки, которую никто
|
||||
* не может прочитать быстрее, чем за кадр.
|
||||
*/
|
||||
private void updateBar(Player p, float tpVal) {
|
||||
final int pct = (int) Math.round(Math.max(0.0D, Math.min(1.0D, (double) tpVal / MAX_TP)) * 100.0D);
|
||||
if (lastPct.getOrDefault(p.getUniqueId(), -1) == pct) {
|
||||
return;
|
||||
}
|
||||
lastPct.put(p.getUniqueId(), pct);
|
||||
|
||||
BossBar bar = bars.get(p.getUniqueId());
|
||||
if (bar == null) {
|
||||
bar = BossBar.bossBar(Component.text("TP 0%"), 0f, BossBar.Color.WHITE, BossBar.Overlay.NOTCHED_10);
|
||||
bars.put(p.getUniqueId(), bar);
|
||||
}
|
||||
float pct = tpVal / MAX_TP;
|
||||
bar.progress(Math.max(0, Math.min(1, pct)));
|
||||
bar.name(Component.text(String.format("TP %.0f%%", pct * 100)).color(NamedTextColor.WHITE));
|
||||
bar.progress(pct / 100.0F);
|
||||
bar.name(Component.text("TP " + pct + "%").color(NamedTextColor.WHITE));
|
||||
bar.color(BossBar.Color.WHITE);
|
||||
if (tpVal > 0) bar.addViewer(p); else bar.removeViewer(p);
|
||||
}
|
||||
|
||||
public void showFor(Player p) {
|
||||
BossBar bar = bars.get(p.getUniqueId());
|
||||
if (bar == null) {
|
||||
updateBar(p, getTP(p));
|
||||
bar = bars.get(p.getUniqueId());
|
||||
}
|
||||
// Полосу создаём здесь, а не через updateBar: updateBar выходит раньше,
|
||||
// если процент не изменился, и полоса тогда осталась бы null.
|
||||
BossBar bar = bars.computeIfAbsent(p.getUniqueId(), id ->
|
||||
BossBar.bossBar(Component.text("TP 0%"), 0f, BossBar.Color.WHITE, BossBar.Overlay.NOTCHED_10));
|
||||
if (getTP(p) > 0) bar.addViewer(p);
|
||||
}
|
||||
|
||||
@@ -155,16 +172,31 @@ public class TensionManager implements Listener {
|
||||
}
|
||||
|
||||
public void tickBattle() {
|
||||
long now = System.currentTimeMillis();
|
||||
for (var e : new java.util.HashMap<>(battleEnemies).entrySet()) {
|
||||
UUID pid = e.getKey();
|
||||
java.util.Set<UUID> set = e.getValue();
|
||||
final long now = System.currentTimeMillis();
|
||||
// Ключи на удаление собираются в список и применяются после обхода.
|
||||
// Раньше здесь на каждый тик делалась полная копия battleEnemies вместе
|
||||
// со всеми вложенными сетами — ради того, чтобы итерировать и одновременно
|
||||
// удалять из той же карты. Это 20 полных аллокаций карты в секунду
|
||||
// на каждого боя, и копия тем больше, чем длиннее бой.
|
||||
java.util.List<UUID> drop = null;
|
||||
for (var e : battleEnemies.entrySet()) {
|
||||
final UUID pid = e.getKey();
|
||||
final java.util.Set<UUID> set = e.getValue();
|
||||
set.removeIf(uuid -> {
|
||||
var ent = org.bukkit.Bukkit.getEntity(uuid);
|
||||
return ent == null || !ent.isValid() || (ent instanceof org.bukkit.entity.LivingEntity le && le.isDead());
|
||||
final var ent = org.bukkit.Bukkit.getEntity(uuid);
|
||||
return ent == null || !ent.isValid()
|
||||
|| (ent instanceof org.bukkit.entity.LivingEntity le && le.isDead());
|
||||
});
|
||||
Long last = lastCombat.get(pid);
|
||||
if (set.isEmpty() || (last != null && now - last > 180000)) {
|
||||
final Long last = lastCombat.get(pid);
|
||||
if (set.isEmpty() || (last != null && now - last > BATTLE_EXPIRE_MS)) {
|
||||
if (drop == null) {
|
||||
drop = new java.util.ArrayList<>(4);
|
||||
}
|
||||
drop.add(pid);
|
||||
}
|
||||
}
|
||||
if (drop != null) {
|
||||
for (final UUID pid : drop) {
|
||||
battleEnemies.remove(pid);
|
||||
lastCombat.remove(pid);
|
||||
}
|
||||
@@ -224,6 +256,7 @@ public class TensionManager implements Listener {
|
||||
// унести полосу в памяти до следующего рестарта.
|
||||
tp.remove(id);
|
||||
lastCombat.remove(id);
|
||||
lastPct.remove(id);
|
||||
AbilityCooldown.clearAll(e.getPlayer());
|
||||
}
|
||||
|
||||
@@ -265,6 +298,9 @@ public class TensionManager implements Listener {
|
||||
public void onDeath(PlayerDeathEvent e) {
|
||||
Player p = e.getEntity();
|
||||
tp.put(p.getUniqueId(), 0f);
|
||||
// Синхронизируем кеш: иначе после смерти updateBar увидит прежний
|
||||
// процент, решит что ничего не изменилось, и не перерисует полосу.
|
||||
lastPct.put(p.getUniqueId(), 0);
|
||||
BossBar bar = bars.get(p.getUniqueId());
|
||||
if (bar != null) { bar.progress(0); bar.name(Component.text("TP 0%")); bar.removeViewer(p); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user