Refactor lobby particle runtime for multi-entry particles

Tick particles from per-player cached always/move lists, drop the
per-instance listeners in Delayed and RandomParticle, and compute
particle viewers once per tick instead of once per spawned point.
This commit is contained in:
2026-10-26 09:47:43 +01:00
parent bfbf98c1d6
commit ef041272bb
9 changed files with 119 additions and 82 deletions
@@ -0,0 +1,61 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle;
import java.util.ArrayList;
import java.util.List;
public class CompiledSet {
private final List<ParticleElement> always = new ArrayList<>();
private final List<ParticleElement> move = new ArrayList<>();
public static CompiledSet of(ParticleElement... particleElements) {
CompiledSet compiledSet = new CompiledSet();
for (ParticleElement particleElement : particleElements) {
compiledSet.add(particleElement);
}
return compiledSet;
}
public void add(ParticleElement particleElement) {
if (particleElement.tickType() == ParticleTickType.ALWAYS) {
always.add(particleElement);
} else {
move.add(particleElement);
}
}
public boolean hasAlways() {
return !always.isEmpty();
}
public boolean hasMove() {
return !move.isEmpty();
}
public void tickAlways(ParticleTickData particleTickData) {
always.forEach(particleElement -> particleElement.tick(particleTickData));
}
public void tickMove(ParticleTickData particleTickData) {
move.forEach(particleElement -> particleElement.tick(particleTickData));
}
}
@@ -19,9 +19,7 @@
package de.steamwar.lobby.particle;
import org.bukkit.Bukkit;
import org.bukkit.Color;
import org.bukkit.Location;
import org.bukkit.Particle;
import org.bukkit.entity.Player;
@@ -47,20 +45,18 @@ public interface ParticleElement {
return new Particle.DustOptions(Color.fromRGB(r, g, b), (float) (RANDOM.nextFloat() / 2 + 0.5));
}
default void display(Location location, Player root, boolean onlySelf, boolean onlyOther, Consumer<Player> consumer) {
if (onlySelf) {
default void display(ParticleTickData particleTickData, Consumer<Player> consumer) {
Player root = particleTickData.getPlayer();
if (particleTickData.isOnlySelf()) {
consumer.accept(root);
return;
}
Bukkit.getOnlinePlayers().forEach(player -> {
if (onlyOther && player == root) {
return;
for (Player player : particleTickData.getViewers()) {
if (particleTickData.isOnlyOthers() && player == root) {
continue;
}
int viewDistance = player.getClientViewDistance() * 16;
if (location.distanceSquared(player.getLocation()) <= viewDistance * viewDistance) {
consumer.accept(player);
}
});
consumer.accept(player);
}
}
default void aggregateAttributes(ParticleData particleData) {
@@ -27,6 +27,7 @@ import de.steamwar.lobby.util.LobbyPlayer;
import de.steamwar.sql.SteamwarUser;
import de.steamwar.sql.UserPerm;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
@@ -36,7 +37,9 @@ import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Linked
@@ -58,14 +61,9 @@ public class ParticleListener implements Listener {
Bukkit.getOnlinePlayers().forEach(player -> {
if (disabled(player)) return;
if (JumpAndRun.isPlayerInJumpAndRun(player)) return;
LobbyPlayer lobbyPlayer = LobbyPlayer.getLobbyPlayer(player.getUniqueId());
ParticleEnum particle = lobbyPlayer.getParticle();
if (particle == null) return;
ParticleData particleData = particle.getParticle();
ParticleElement particleElement = particleData.getParticleElement();
if (particleElement.tickType() == ParticleTickType.ALWAYS) {
particleElement.tick(new ParticleTickData(player.getWorld(), player, deg, movingPlayers.containsKey(player)));
}
CompiledSet compiledSet = LobbyPlayer.getLobbyPlayer(player.getUniqueId()).getCompiledSet();
if (compiledSet == null || !compiledSet.hasAlways()) return;
compiledSet.tickAlways(new ParticleTickData(player.getWorld(), player, deg, movingPlayers.containsKey(player), viewers(player)));
});
}, 0, 1);
}
@@ -85,14 +83,11 @@ public class ParticleListener implements Listener {
Player player = event.getPlayer();
if (disabled(player)) return;
if (JumpAndRun.isPlayerInJumpAndRun(player)) return;
LobbyPlayer lobbyPlayer = LobbyPlayer.getLobbyPlayer(player.getUniqueId());
ParticleEnum particle = lobbyPlayer.getParticle();
if (particle == null) return;
CompiledSet compiledSet = LobbyPlayer.getLobbyPlayer(player.getUniqueId()).getCompiledSet();
if (compiledSet == null) return;
ParticleData particleData = particle.getParticle();
ParticleElement particleElement = particleData.getParticleElement();
if (particleElement.tickType() == ParticleTickType.MOVE) {
particleElement.tick(new ParticleTickData(player.getWorld(), player, deg, true));
if (compiledSet.hasMove()) {
compiledSet.tickMove(new ParticleTickData(player.getWorld(), player, deg, true, viewers(player)));
}
if (event.getFrom().getX() != event.getTo().getX() || event.getFrom().getY() != event.getTo().getY() || event.getFrom().getZ() != event.getTo().getZ()) {
movingPlayers.put(player, 5);
@@ -111,6 +106,18 @@ public class ParticleListener implements Listener {
}
}
private static List<Player> viewers(Player root) {
Location location = root.getLocation();
List<Player> viewers = new ArrayList<>();
for (Player player : root.getWorld().getPlayers()) {
int viewDistance = player.getClientViewDistance() * 16;
if (location.distanceSquared(player.getLocation()) <= viewDistance * viewDistance) {
viewers.add(player);
}
}
return viewers;
}
public static boolean disabled(Player player) {
SteamwarUser user = SteamwarUser.get(player.getUniqueId());
if (Bukkit.getOnlinePlayers().size() > SERVER_TEAM_MAX_SIZE) return true;
@@ -26,6 +26,8 @@ import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import java.util.List;
@RequiredArgsConstructor
@AllArgsConstructor
@Getter
@@ -36,6 +38,7 @@ public class ParticleTickData {
private Location location;
private final double deg;
private final boolean isMoving;
private final List<Player> viewers;
private boolean onlySelf = false;
private boolean onlyOthers = false;
@@ -65,6 +68,6 @@ public class ParticleTickData {
}
public ParticleTickData copy() {
return new ParticleTickData(world, player, location, deg, isMoving, onlySelf, onlyOthers);
return new ParticleTickData(world, player, location, deg, isMoving, viewers, onlySelf, onlyOthers);
}
}
@@ -19,35 +19,21 @@
package de.steamwar.lobby.particle.elements;
import de.steamwar.lobby.LobbySystem;
import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.ParticleTickData;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
public class Delayed extends DelegatingParticleElement {
private DelayedData delayedData = new DelayedData();
// Weak keys drop players once they quit, so no quit listener has to be registered per instance
private final Map<Player, Integer> data = new WeakHashMap<>();
private int interval;
private static class DelayedData implements Listener {
private Map<Player, Integer> data = new HashMap<>();
@EventHandler(ignoreCancelled = true)
public void onPlayerQuit(PlayerQuitEvent event) {
data.remove(event.getPlayer());
}
}
public Delayed(ParticleElement particleElement, int interval) {
super(particleElement);
Bukkit.getPluginManager().registerEvents(delayedData, LobbySystem.getInstance());
this.interval = interval;
if (interval <= 0) {
throw new IllegalArgumentException("Interval must be greater than 0");
@@ -61,8 +47,8 @@ public class Delayed extends DelegatingParticleElement {
@Override
public void tick(ParticleTickData particleTickData) {
int currentNumber = delayedData.data.getOrDefault(particleTickData.getPlayer(), 0) % interval;
delayedData.data.put(particleTickData.getPlayer(), currentNumber + 1);
int currentNumber = data.getOrDefault(particleTickData.getPlayer(), 0) % interval;
data.put(particleTickData.getPlayer(), currentNumber + 1);
if (currentNumber != 0) {
return;
}
@@ -71,7 +71,7 @@ public class DustParticle implements ParticleElement {
} else {
dustOptions = randomParticleDust();
}
display(location, particleTickData.getPlayer(), particleTickData.isOnlySelf(), particleTickData.isOnlyOthers(), player -> {
display(particleTickData, player -> {
player.spawnParticle(particle, location, count, vx, vy, vz, speed, dustOptions);
});
}
@@ -19,58 +19,30 @@
package de.steamwar.lobby.particle.elements;
import de.steamwar.lobby.LobbySystem;
import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.ParticleEnum;
import de.steamwar.lobby.particle.ParticleTickData;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.WeakHashMap;
public class RandomParticle implements ParticleElement {
private RandomData randomData;
private ParticleEnum[] particleEnums;
private static class RandomData implements Listener {
private Random random = new Random();
private int max;
private Map<Player, Integer> data = new HashMap<>();
public RandomData(int max) {
this.max = max;
}
@EventHandler(ignoreCancelled = true)
public void onPlayerJoin(PlayerJoinEvent event) {
data.put(event.getPlayer(), random.nextInt(max));
}
@EventHandler(ignoreCancelled = true)
public void onPlayerQuit(PlayerQuitEvent event) {
data.remove(event.getPlayer());
}
}
// Weak keys drop players once they quit, so each player rolls again on their next session
private final Map<Player, Integer> data = new WeakHashMap<>();
private final ParticleEnum[] particleEnums;
public RandomParticle(ParticleEnum... particleEnums) {
if (particleEnums.length == 0) {
throw new IllegalArgumentException("ParticleEnums must not be empty");
}
this.randomData = new RandomData(particleEnums.length);
Bukkit.getPluginManager().registerEvents(randomData, LobbySystem.getInstance());
this.particleEnums = particleEnums;
}
@Override
public void tick(ParticleTickData particleTickData) {
int currentNumber = randomData.data.getOrDefault(particleTickData.getPlayer(), 0);
int currentNumber = data.computeIfAbsent(particleTickData.getPlayer(), player -> RANDOM.nextInt(particleEnums.length));
particleEnums[currentNumber].getParticle().getParticleElement().tick(particleTickData);
}
}
@@ -74,7 +74,7 @@ public class SimpleParticle implements ParticleElement {
@Override
public void tick(ParticleTickData particleTickData) {
Location location = particleTickData.getLocation().add(0.0, 0.2, 0.0);
display(location, particleTickData.getPlayer(), particleTickData.isOnlySelf(), particleTickData.isOnlyOthers(), player -> {
display(particleTickData, player -> {
if (customVelocity) {
player.spawnParticle(particle, location, count, vx, vy, vz, time);
} else {
@@ -19,6 +19,7 @@
package de.steamwar.lobby.util;
import de.steamwar.lobby.particle.CompiledSet;
import de.steamwar.lobby.particle.ParticleEnum;
import de.steamwar.lobby.particle.ParticleInventory;
import de.steamwar.sql.SteamwarUser;
@@ -34,6 +35,7 @@ public class LobbyPlayer {
private static Map<UUID, LobbyPlayer> cache = new HashMap<>();
private ParticleEnum particle;
private CompiledSet compiledSet;
private int userId;
private boolean fly;
@@ -46,6 +48,7 @@ public class LobbyPlayer {
if (saved != null) {
particle = ParticleInventory.convertFromString(saved);
}
compile();
}
public boolean isFlying() {
@@ -68,6 +71,15 @@ public class LobbyPlayer {
UserConfig.updatePlayerConfig(userId, "lobby-particle", saved);
}
this.particle = particle;
compile();
}
public CompiledSet getCompiledSet() {
return compiledSet;
}
private void compile() {
compiledSet = particle == null ? null : CompiledSet.of(particle.getParticle().getParticleElement());
}
public static LobbyPlayer getLobbyPlayer(UUID uuid) {