From 471417708ce425275c194f9da7466630837e028a Mon Sep 17 00:00:00 2001 From: Pablo Herrera Date: Tue, 23 Apr 2024 14:28:43 +0700 Subject: [PATCH] Optimize tnt render by ignoring afk players (#1310) Signed-off-by: Pablo Herrera --- core/src/main/java/tc/oc/pgm/PGMPlugin.java | 11 +- core/src/main/java/tc/oc/pgm/api/PGM.java | 3 + .../main/java/tc/oc/pgm/api/party/Party.java | 15 +++ .../tc/oc/pgm/api/player/MatchPlayer.java | 30 +++++ .../java/tc/oc/pgm/goals/GoalMatchModule.java | 5 +- .../main/java/tc/oc/pgm/match/MatchImpl.java | 13 +-- .../java/tc/oc/pgm/match/MatchPlayerImpl.java | 17 +++ .../pgm/tntrender/TNTRenderMatchModule.java | 18 ++- .../tc/oc/pgm/util/listener/AfkTracker.java | 104 ++++++++++++++++++ 9 files changed, 201 insertions(+), 15 deletions(-) create mode 100644 util/src/main/java/tc/oc/pgm/util/listener/AfkTracker.java diff --git a/core/src/main/java/tc/oc/pgm/PGMPlugin.java b/core/src/main/java/tc/oc/pgm/PGMPlugin.java index 12ad5b4b9d..73aa228373 100644 --- a/core/src/main/java/tc/oc/pgm/PGMPlugin.java +++ b/core/src/main/java/tc/oc/pgm/PGMPlugin.java @@ -71,6 +71,7 @@ import tc.oc.pgm.util.chunk.NullChunkGenerator; import tc.oc.pgm.util.compatability.SportPaperListener; import tc.oc.pgm.util.concurrent.BukkitExecutorService; +import tc.oc.pgm.util.listener.AfkTracker; import tc.oc.pgm.util.listener.ItemTransferListener; import tc.oc.pgm.util.listener.PlayerBlockListener; import tc.oc.pgm.util.listener.PlayerMoveListener; @@ -97,6 +98,7 @@ public class PGMPlugin extends JavaPlugin implements PGM, Listener { private ScheduledExecutorService executorService; private ScheduledExecutorService asyncExecutorService; private InventoryManager inventoryManager; + private AfkTracker afkTracker; public PGMPlugin() { super(); @@ -220,7 +222,7 @@ public void onEnable() { Integration.setVanishIntegration(new SimpleVanishIntegration(matchManager, executorService)); inventoryManager = new InventoryManager(this); - inventoryManager.init(); + afkTracker = new AfkTracker(this); if (config.showTabList()) { matchTabManager = new MatchTabManager(this); @@ -339,6 +341,11 @@ public InventoryManager getInventoryManager() { return inventoryManager; } + @Override + public AfkTracker getAfkTracker() { + return afkTracker; + } + private void registerCommands() { try { new PGMCommandGraph(this); @@ -363,6 +370,8 @@ private void registerListeners() { registerEvents(new TNTMinecartPlacementListener()); new BlockTransformListener(this).registerEvents(); registerEvents(matchManager); + inventoryManager.init(); + registerEvents(afkTracker); if (matchTabManager != null) registerEvents(matchTabManager); registerEvents(nameDecorationRegistry); registerEvents(new PGMListener(this, matchManager)); diff --git a/core/src/main/java/tc/oc/pgm/api/PGM.java b/core/src/main/java/tc/oc/pgm/api/PGM.java index e7177396bb..6aa52778c2 100644 --- a/core/src/main/java/tc/oc/pgm/api/PGM.java +++ b/core/src/main/java/tc/oc/pgm/api/PGM.java @@ -13,6 +13,7 @@ import tc.oc.pgm.api.match.MatchManager; import tc.oc.pgm.namedecorations.NameDecorationRegistry; import tc.oc.pgm.tablist.MatchTabManager; +import tc.oc.pgm.util.listener.AfkTracker; /** PvP Game Manager (aka. PGM), the global {@link Plugin} to manage PvP games. */ public interface PGM extends Plugin { @@ -40,6 +41,8 @@ public interface PGM extends Plugin { InventoryManager getInventoryManager(); + AfkTracker getAfkTracker(); + AtomicReference GLOBAL = new AtomicReference<>(null); static PGM set(PGM pgm) { diff --git a/core/src/main/java/tc/oc/pgm/api/party/Party.java b/core/src/main/java/tc/oc/pgm/api/party/Party.java index 266890f3ed..bd94980801 100644 --- a/core/src/main/java/tc/oc/pgm/api/party/Party.java +++ b/core/src/main/java/tc/oc/pgm/api/party/Party.java @@ -1,5 +1,6 @@ package tc.oc.pgm.api.party; +import java.util.ArrayList; import java.util.Collection; import java.util.UUID; import net.kyori.adventure.text.Component; @@ -158,6 +159,20 @@ default Collection> getFilterableChi return this.getPlayers(); } + @Override + @SuppressWarnings("unchecked") + default > Collection getFilterableDescendants( + Class type) { + Collection result = new ArrayList<>(); + if (type.isAssignableFrom(Party.class)) { + result.add((R) this); + } + if (type.isAssignableFrom(MatchPlayer.class)) { + result.addAll((Collection) getPlayers()); + } + return result; + } + @Override default Party getParty() { return this; diff --git a/core/src/main/java/tc/oc/pgm/api/player/MatchPlayer.java b/core/src/main/java/tc/oc/pgm/api/player/MatchPlayer.java index ba803b06e3..7711521fb9 100644 --- a/core/src/main/java/tc/oc/pgm/api/player/MatchPlayer.java +++ b/core/src/main/java/tc/oc/pgm/api/player/MatchPlayer.java @@ -1,5 +1,7 @@ package tc.oc.pgm.api.player; +import java.time.Duration; +import java.time.Instant; import java.util.List; import java.util.UUID; import org.bukkit.GameMode; @@ -20,6 +22,7 @@ import tc.oc.pgm.util.attribute.Attribute; import tc.oc.pgm.util.attribute.AttributeInstance; import tc.oc.pgm.util.bukkit.ViaUtils; +import tc.oc.pgm.util.listener.AfkTracker; import tc.oc.pgm.util.named.Named; /** @@ -144,6 +147,33 @@ default boolean isLegacy() { return getProtocolVersion() <= ViaUtils.VERSION_1_7; } + /** + * Get when the {@link MatchPlayer} was last active in the game + * + * @return the last time player was not afk + */ + default Instant getLastActive() { + return getActivity().getLastActive(); + } + + /** + * Get whether the {@link MatchPlayer} is actively moving, or afk + * + * @param duration How much time until the player is considered inactive + * @return true if the player moved within {@param duration}, false if the player has been afk + * that long + */ + default boolean isActive(Duration duration) { + return getActivity().isActive(duration); + } + + /** + * Get the AFK activity tracker for the {@link MatchPlayer} + * + * @return the afk activity tracker + */ + AfkTracker.Activity getActivity(); + /** * Get whether the {@link MatchPlayer} can interact with things in the {@link Match}. * diff --git a/core/src/main/java/tc/oc/pgm/goals/GoalMatchModule.java b/core/src/main/java/tc/oc/pgm/goals/GoalMatchModule.java index 5b5d13169d..dadbe35a15 100644 --- a/core/src/main/java/tc/oc/pgm/goals/GoalMatchModule.java +++ b/core/src/main/java/tc/oc/pgm/goals/GoalMatchModule.java @@ -7,6 +7,7 @@ import com.google.common.collect.Multimap; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -59,11 +60,11 @@ private GoalMatchModule(Match match) { } public Collection getGoals() { - return goals; + return Collections.unmodifiableCollection(goals); } public Collection getGoals(Competitor competitor) { - return goalsByCompetitor.get(competitor); + return Collections.unmodifiableCollection(goalsByCompetitor.get(competitor)); } public Collection getCompetitors(Goal goal) { diff --git a/core/src/main/java/tc/oc/pgm/match/MatchImpl.java b/core/src/main/java/tc/oc/pgm/match/MatchImpl.java index f29548ceed..f9372a172d 100644 --- a/core/src/main/java/tc/oc/pgm/match/MatchImpl.java +++ b/core/src/main/java/tc/oc/pgm/match/MatchImpl.java @@ -19,7 +19,6 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.stream.Collectors; import net.kyori.adventure.text.Component; import org.bukkit.Bukkit; import org.bukkit.World; @@ -407,7 +406,7 @@ public void setMaxPlayers(int players) { @Override public Collection getPlayers() { - return ImmutableList.copyOf(players.values()); + return Collections.unmodifiableCollection(players.values()); } @Override @@ -937,17 +936,15 @@ public Collection> getFilterableChild @Override @SuppressWarnings("unchecked") public > Collection getFilterableDescendants(Class type) { - final Collection result = new LinkedList<>(); + Collection result = new ArrayList<>(); if (type.isAssignableFrom(Match.class)) { result.add((R) this); } - if (Party.class.isAssignableFrom(type)) { - result.addAll( - (List) - this.getParties().stream().filter(type::isInstance).collect(Collectors.toList())); + if (type.isAssignableFrom(Party.class)) { + result.addAll((Collection) getParties()); } if (type.isAssignableFrom(MatchPlayer.class)) { - result.addAll((List) this.getPlayers()); + result.addAll((Collection) getPlayers()); } return result; } diff --git a/core/src/main/java/tc/oc/pgm/match/MatchPlayerImpl.java b/core/src/main/java/tc/oc/pgm/match/MatchPlayerImpl.java index 31df025d1e..1d95e5aed7 100644 --- a/core/src/main/java/tc/oc/pgm/match/MatchPlayerImpl.java +++ b/core/src/main/java/tc/oc/pgm/match/MatchPlayerImpl.java @@ -56,6 +56,7 @@ import tc.oc.pgm.util.attribute.AttributeMap; import tc.oc.pgm.util.attribute.AttributeModifier; import tc.oc.pgm.util.bukkit.ViaUtils; +import tc.oc.pgm.util.listener.AfkTracker; import tc.oc.pgm.util.named.NameStyle; import tc.oc.pgm.util.nms.NMSHacks; @@ -79,6 +80,7 @@ public class MatchPlayerImpl implements MatchPlayer, Comparable { private final AtomicBoolean protocolReady; private final AtomicInteger protocolVersion; private final AttributeMap attributeMap; + private final AfkTracker.Activity activity; public MatchPlayerImpl(Match match, Player player) { this.logger = @@ -95,6 +97,7 @@ public MatchPlayerImpl(Match match, Player player) { this.protocolReady = new AtomicBoolean(ViaUtils.isReady(player)); this.protocolVersion = new AtomicInteger(ViaUtils.getProtocolVersion(player)); this.attributeMap = NMSHacks.buildAttributeMap(player); + this.activity = PGM.get().getAfkTracker().getActivity(player); } @Override @@ -190,6 +193,11 @@ public boolean isFrozen() { return frozen.get(); } + @Override + public AfkTracker.Activity getActivity() { + return activity; + } + @Override public boolean canInteract() { return isAlive() && !isFrozen(); @@ -471,6 +479,15 @@ public Collection> getFilterableChil return Collections.emptyList(); } + @Override + @SuppressWarnings("unchecked") + public > Collection getFilterableDescendants(Class type) { + if (type.isAssignableFrom(getClass())) { + return Collections.singleton((R) this); + } + return Collections.emptyList(); + } + @Override public int compareTo(MatchPlayer o) { final int diff = this.id.compareTo(o.getId()); diff --git a/core/src/main/java/tc/oc/pgm/tntrender/TNTRenderMatchModule.java b/core/src/main/java/tc/oc/pgm/tntrender/TNTRenderMatchModule.java index a405711bdf..49cdddb7f2 100644 --- a/core/src/main/java/tc/oc/pgm/tntrender/TNTRenderMatchModule.java +++ b/core/src/main/java/tc/oc/pgm/tntrender/TNTRenderMatchModule.java @@ -1,5 +1,6 @@ package tc.oc.pgm.tntrender; +import java.time.Duration; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -10,6 +11,7 @@ import org.bukkit.Material; import org.bukkit.entity.TNTPrimed; import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.entity.EntityExplodeEvent; import org.bukkit.event.entity.ExplosionPrimeEvent; @@ -18,11 +20,12 @@ import tc.oc.pgm.api.match.MatchScope; import tc.oc.pgm.api.player.MatchPlayer; import tc.oc.pgm.events.ListenerScope; +import tc.oc.pgm.util.event.block.BlockDispenseEntityEvent; import tc.oc.pgm.util.nms.NMSHacks; @ListenerScope(value = MatchScope.LOADED) public class TNTRenderMatchModule implements MatchModule, Listener { - + private static final Duration AFK_TIME = Duration.ofSeconds(30); private static final double MAX_DISTANCE = Math.pow(64d, 2); private final Match match; @@ -41,13 +44,19 @@ public void enable() { () -> entities.removeIf(PrimedTnt::update), 0, 50, TimeUnit.MILLISECONDS); } - @EventHandler + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onTntSpawn(ExplosionPrimeEvent event) { if (event.getEntity() instanceof TNTPrimed) entities.add(new PrimedTnt((TNTPrimed) event.getEntity())); } - @EventHandler + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onDispense(BlockDispenseEntityEvent event) { + if (event.getEntity() instanceof TNTPrimed) + entities.add(new PrimedTnt((TNTPrimed) event.getEntity())); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onTntExplode(EntityExplodeEvent event) { if (!(event.getEntity() instanceof TNTPrimed)) return; Location explosion = event.getLocation(); @@ -90,7 +99,8 @@ public boolean update() { } private void updatePlayer(MatchPlayer player) { - if (currentLocation.distanceSquared(player.getBukkit().getLocation()) >= MAX_DISTANCE) { + if (currentLocation.distanceSquared(player.getLocation()) >= MAX_DISTANCE + && player.isActive(AFK_TIME)) { if (viewers.add(player)) { NMSHacks.sendBlockChange(currentLocation, player.getBukkit(), Material.TNT); } else if (moved) { diff --git a/util/src/main/java/tc/oc/pgm/util/listener/AfkTracker.java b/util/src/main/java/tc/oc/pgm/util/listener/AfkTracker.java new file mode 100644 index 0000000000..850f18b67a --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/listener/AfkTracker.java @@ -0,0 +1,104 @@ +package tc.oc.pgm.util.listener; + +import java.time.Duration; +import java.time.Instant; +import java.util.Map; +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.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.event.inventory.InventoryOpenEvent; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.plugin.java.JavaPlugin; +import tc.oc.pgm.util.bukkit.OnlinePlayerMapAdapter; + +public class AfkTracker implements Listener { + + private final Map activityMap; + + // By recycling cached instants we avoid creating a ton of objects. + private Instant now = Instant.now(); + + public AfkTracker(JavaPlugin plugin) { + this.activityMap = new OnlinePlayerMapAdapter<>(plugin); + Bukkit.getScheduler().scheduleSyncRepeatingTask(plugin, () -> now = Instant.now(), 0L, 5L); + } + + public Activity getActivity(Player player) { + return activityMap.computeIfAbsent(player, pl -> new Activity()); + } + + private void track(Player player) { + getActivity(player).lastActive = now; + } + + @EventHandler(priority = EventPriority.LOWEST) + public void onPlayerJoin(PlayerJoinEvent event) { + track(event.getPlayer()); + } + + @EventHandler + public void onPlayerMove(PlayerMoveEvent event) { + if (event.getFrom().getYaw() != event.getTo().getYaw() + && event.getFrom().getPitch() != event.getTo().getPitch()) { + track(event.getPlayer()); + } + } + + @EventHandler + public void onPlayerChat(AsyncPlayerChatEvent event) { + track(event.getPlayer()); + } + + @EventHandler + public void onCommand(PlayerCommandPreprocessEvent event) { + track(event.getPlayer()); + } + + @EventHandler + public void onPlayerInteract(PlayerInteractEvent event) { + track(event.getPlayer()); + } + + @EventHandler + public void onInventoryOpen(InventoryOpenEvent event) { + if (event.getPlayer() instanceof Player) track((Player) event.getPlayer()); + } + + @EventHandler + public void onInventoryClick(InventoryClickEvent event) { + if (event.getWhoClicked() instanceof Player) track((Player) event.getWhoClicked()); + } + + @EventHandler + public void onInventoryClose(InventoryCloseEvent event) { + if (event.getPlayer() instanceof Player) track((Player) event.getPlayer()); + } + + public class Activity { + private Instant lastActive = now; + + public Instant getLastActive() { + return lastActive; + } + + public Duration getAfkDuration() { + return Duration.between(lastActive, now); + } + + public boolean isAfk(Duration duration) { + return getAfkDuration().compareTo(duration) >= 0; + } + + public boolean isActive(Duration duration) { + return getAfkDuration().compareTo(duration) < 0; + } + } +}