From 5d0333b823873ad189ab850ef58ca242c5b24984 Mon Sep 17 00:00:00 2001 From: Artemis-the-gr8 Date: Sat, 4 Jun 2022 22:03:57 +0200 Subject: [PATCH] Caught the ConcurrentModificationException and added a bunch of logging-statements for testing --- .../the/gr8/playerstats/ThreadManager.java | 9 +- .../filehandlers/TestFileHandler.java | 105 +++++++++++++----- .../gr8/playerstats/reload/ReloadAction.java | 48 ++++++-- .../gr8/playerstats/reload/ReloadThread.java | 22 +++- .../gr8/playerstats/statistic/StatThread.java | 33 ++++-- .../playerstats/statistic/TopStatAction.java | 40 +++---- .../utils/OfflinePlayerHandler.java | 4 +- 7 files changed, 185 insertions(+), 76 deletions(-) diff --git a/src/main/java/com/gmail/artemis/the/gr8/playerstats/ThreadManager.java b/src/main/java/com/gmail/artemis/the/gr8/playerstats/ThreadManager.java index 1f2bd3f..f961790 100644 --- a/src/main/java/com/gmail/artemis/the/gr8/playerstats/ThreadManager.java +++ b/src/main/java/com/gmail/artemis/the/gr8/playerstats/ThreadManager.java @@ -1,6 +1,7 @@ package com.gmail.artemis.the.gr8.playerstats; import com.gmail.artemis.the.gr8.playerstats.filehandlers.ConfigHandler; +import com.gmail.artemis.the.gr8.playerstats.filehandlers.TestFileHandler; import com.gmail.artemis.the.gr8.playerstats.reload.ReloadThread; import com.gmail.artemis.the.gr8.playerstats.statistic.StatRequest; import com.gmail.artemis.the.gr8.playerstats.statistic.StatThread; @@ -11,9 +12,12 @@ import org.bukkit.command.CommandSender; public class ThreadManager { + private static final int threshold = 10; + private final Main plugin; private final BukkitAudiences adventure; private static ConfigHandler config; + private static TestFileHandler testFile; private final MessageFactory messageFactory; private ReloadThread reloadThread; @@ -26,16 +30,17 @@ public class ThreadManager { config = c; messageFactory = m; + testFile = new TestFileHandler(plugin); startReloadThread(null, true); } public void startReloadThread(CommandSender sender, boolean firstTimeLoading) { - reloadThread = new ReloadThread(config, plugin, statThread, sender, firstTimeLoading); + reloadThread = new ReloadThread(threshold, config, testFile, plugin, statThread, sender, firstTimeLoading); reloadThread.start(); } public void startStatThread(StatRequest request) { - statThread = new StatThread(request, reloadThread, adventure, config, messageFactory, plugin); + statThread = new StatThread(threshold, request, reloadThread, adventure, config, testFile, messageFactory, plugin); statThread.start(); } diff --git a/src/main/java/com/gmail/artemis/the/gr8/playerstats/filehandlers/TestFileHandler.java b/src/main/java/com/gmail/artemis/the/gr8/playerstats/filehandlers/TestFileHandler.java index b9dcdab..0ede62e 100644 --- a/src/main/java/com/gmail/artemis/the/gr8/playerstats/filehandlers/TestFileHandler.java +++ b/src/main/java/com/gmail/artemis/the/gr8/playerstats/filehandlers/TestFileHandler.java @@ -11,28 +11,43 @@ import java.io.IOException; public class TestFileHandler { - private static File testFile; - private static FileConfiguration testConf; - private static ConfigurationSection playerCount; + private File testFile; + private FileConfiguration testConf; + private ConfigurationSection number; private final Main plugin; + private String onEnable; + private String reload; + private String debugging; + private String topStat; public TestFileHandler(Main p) { plugin = p; - loadFile(); + onEnable = "onEnable"; + reload = "reload"; + debugging = "exception-debugging"; + topStat = "top-stat"; } - public static void savePlayerCount(int count) { + /** + * Creates a new config section for the given threshold. Only needs to be called once, unless threshold changes. + * @param count amount of players to calculate statistics with + * @param threshold how small the subTasks have to become + */ + public void saveThreshold(int count, int threshold) { + loadFile(count); + String path = threshold + " threshold"; try { - playerCount = testConf.getConfigurationSection(count + " players"); - if (playerCount == null) { - playerCount = testConf.createSection(count + " players"); - playerCount.createSection("onEnable"); - playerCount.createSection("individual-stat"); - playerCount.createSection("top-stat"); + number = testConf.getConfigurationSection(path); + if (number == null) { + number = testConf.createSection(path); + number.createSection(onEnable); + number.createSection(reload); + number.createSection(debugging); + number.createSection(topStat); } else { - playerCount = testConf.getConfigurationSection(count + " players"); + number = testConf.getConfigurationSection(path); } saveFile(); } @@ -41,18 +56,49 @@ public class TestFileHandler { } } - public static void saveTimeTaken(long time, String timeDescription) { + public void logRunCount(boolean errorEncountered) { try { - if (timeDescription.equalsIgnoreCase("onEnable")) { - saveToSection(time, playerCount.getConfigurationSection("onEnable")); + ConfigurationSection section = number.getConfigurationSection(debugging); + if (section != null) { + int runs = section.getInt("runs"); + section.set("runs", runs +1); + + if (errorEncountered) { + int errors = section.getInt("errors"); + section.set("errors", errors + 1); + + String path = "error-" + (errors + 1) + "-during-run"; + int lastError = section.getInt("error-" + errors + "-during-run"); + + int runsUntilError = runs - lastError; + String path2 = "until-error-" + (errors + 1); + + section.set(path2, runsUntilError); + section.set(path, runs); + } saveFile(); } - else if (timeDescription.equalsIgnoreCase("individual-stat")) { - saveToSection(time, playerCount.getConfigurationSection("individual-stat")); - saveFile(); - } - else if (timeDescription.equalsIgnoreCase("top-stat")) { - saveToSection(time, playerCount.getConfigurationSection("top-stat")); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Logs how long a certain method took for the earlier set threshold. Always make sure saveThreshold has been + * called once before this method is called. + * @param time how long the given action took + * @param scenario describes which section to get. 1 means onEnable, 2 means reload, and 3 means top-stat + */ + public void saveTimeTaken(long time, int scenario) { + String path = ""; + if (scenario == 1) path = onEnable; + else if (scenario == 2) path = reload; + else if (scenario == 3) path = topStat; + + try { + ConfigurationSection section = number.getConfigurationSection(path); + if (section != null) { + saveTimeToSection(time, section); saveFile(); } } @@ -61,7 +107,7 @@ public class TestFileHandler { } } - private static void saveToSection(long time, ConfigurationSection section) { + private void saveTimeToSection(long time, ConfigurationSection section) { if (section.contains("average")) { long average = section.getLong("average"); long newAverage = ((average * (section.getKeys(false).size() -1)) + time)/section.getKeys(false).size(); @@ -75,9 +121,11 @@ public class TestFileHandler { } } - private void loadFile() { - testFile = new File(plugin.getDataFolder(), "test.yml"); + private void loadFile(int players) { + String fileName = "test_" + players + ".yml"; + testFile = new File(plugin.getDataFolder(), fileName); if (!testFile.exists()) { + plugin.getLogger().info("Attempting to create testFile..."); createFile(); } @@ -91,24 +139,23 @@ public class TestFileHandler { saveFile(); } - private static void createFile() { - testFile.getParentFile().mkdirs(); + private void createFile() { + testFile.getParentFile().mkdirs(); try { testFile.createNewFile(); + plugin.getLogger().info("Even though this would return false, secretly a file has been created anyway"); } catch (IOException e) { e.printStackTrace(); } } - private static boolean saveFile() { + private void saveFile() { try { testConf.save(testFile); - return true; } catch (Exception e) { e.printStackTrace(); - return false; } } } diff --git a/src/main/java/com/gmail/artemis/the/gr8/playerstats/reload/ReloadAction.java b/src/main/java/com/gmail/artemis/the/gr8/playerstats/reload/ReloadAction.java index 2ba62c3..02217ed 100644 --- a/src/main/java/com/gmail/artemis/the/gr8/playerstats/reload/ReloadAction.java +++ b/src/main/java/com/gmail/artemis/the/gr8/playerstats/reload/ReloadAction.java @@ -1,9 +1,9 @@ package com.gmail.artemis.the.gr8.playerstats.reload; import com.gmail.artemis.the.gr8.playerstats.utils.UnixTimeHandler; +import org.bukkit.Bukkit; import org.bukkit.OfflinePlayer; -import java.util.Arrays; import java.util.ConcurrentModificationException; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -11,18 +11,34 @@ import java.util.concurrent.RecursiveAction; public class ReloadAction extends RecursiveAction { - private static final int threshold = 10; + private final int threshold; private final OfflinePlayer[] players; + private final int start; + private final int end; + private final boolean whitelistOnly; private final boolean excludeBanned; private final int lastPlayedLimit; private final ConcurrentHashMap offlinePlayerUUIDs; - public ReloadAction(OfflinePlayer[] players, + public ReloadAction(int threshold, OfflinePlayer[] players, boolean whitelistOnly, boolean excludeBanned, int lastPlayedLimit, ConcurrentHashMap offlinePlayerUUIDs) { + + this(threshold, players, 0, players.length, + whitelistOnly, excludeBanned, lastPlayedLimit, offlinePlayerUUIDs); + } + + protected ReloadAction(int threshold, OfflinePlayer[] players, int start, int end, + boolean whitelistOnly, boolean excludeBanned, int lastPlayedLimit, + ConcurrentHashMap offlinePlayerUUIDs) { + + this.threshold = threshold; this.players = players; + this.start = start; + this.end = end; + this.whitelistOnly = whitelistOnly; this.excludeBanned = excludeBanned; this.lastPlayedLimit = lastPlayedLimit; @@ -31,13 +47,15 @@ public class ReloadAction extends RecursiveAction { @Override protected void compute() { - if (players.length < threshold) { + final int length = end - start; + if (length < threshold) { process(); } else { - ReloadAction subTask1 = new ReloadAction(Arrays.copyOfRange(players, 0, players.length/2), + final int split = length / 2; + final ReloadAction subTask1 = new ReloadAction(threshold, players, start, (start + split), whitelistOnly, excludeBanned, lastPlayedLimit, offlinePlayerUUIDs); - ReloadAction subTask2 = new ReloadAction(Arrays.copyOfRange(players, players.length/2, players.length), + final ReloadAction subTask2 = new ReloadAction(threshold, players, (start + split), end, whitelistOnly, excludeBanned, lastPlayedLimit, offlinePlayerUUIDs); try { @@ -51,13 +69,19 @@ public class ReloadAction extends RecursiveAction { } private void process() { - for (OfflinePlayer player : players) { - if (player.getName() != null && - (!whitelistOnly || player.isWhitelisted()) && - (!excludeBanned || !player.isBanned()) && - (lastPlayedLimit == 0 || UnixTimeHandler.hasPlayedSince(lastPlayedLimit, player.getLastPlayed()))) { - offlinePlayerUUIDs.put(player.getName(), player.getUniqueId()); + try { + for (int i = start; i < end; i++) { + OfflinePlayer player = players[i]; + if (player.getName() != null && + (!whitelistOnly || player.isWhitelisted()) && + (!excludeBanned || !player.isBanned()) && + (lastPlayedLimit == 0 || UnixTimeHandler.hasPlayedSince(lastPlayedLimit, player.getLastPlayed()))) { + offlinePlayerUUIDs.put(player.getName(), player.getUniqueId()); + } } + } catch (ConcurrentModificationException e) { + Bukkit.getLogger().warning("ReloadAction has thrown a ConcurrentModificationException because of " + e.getCause()); + e.printStackTrace(); } } } diff --git a/src/main/java/com/gmail/artemis/the/gr8/playerstats/reload/ReloadThread.java b/src/main/java/com/gmail/artemis/the/gr8/playerstats/reload/ReloadThread.java index 1c6e719..78ecd45 100644 --- a/src/main/java/com/gmail/artemis/the/gr8/playerstats/reload/ReloadThread.java +++ b/src/main/java/com/gmail/artemis/the/gr8/playerstats/reload/ReloadThread.java @@ -15,19 +15,25 @@ import org.jetbrains.annotations.Nullable; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ForkJoinPool; public class ReloadThread extends Thread { + private final int threshold; + private static ConfigHandler config; + private static TestFileHandler testFile; private final Main plugin; private final StatThread statThread; private final CommandSender sender; private final boolean firstTimeLoading; - public ReloadThread(ConfigHandler c, Main p, @Nullable StatThread s, @Nullable CommandSender se, boolean firstTime) { + public ReloadThread(int threshold, ConfigHandler c, TestFileHandler t, Main p, @Nullable StatThread s, @Nullable CommandSender se, boolean firstTime) { + this.threshold = threshold; config = c; + testFile = t; plugin = p; statThread = s; @@ -55,6 +61,7 @@ public class ReloadThread extends Thread { if (config.reloadConfig()) { OfflinePlayerHandler.updateOfflinePlayerList(getPlayerMap(false)); + testFile.saveTimeTaken(System.currentTimeMillis() - time, 2); plugin.getLogger().info("Amount of relevant players: " + OfflinePlayerHandler.getOfflinePlayerCount()); plugin.logTimeTaken("ReloadThread", "loading offline players", time); if (sender != null) { @@ -66,8 +73,8 @@ public class ReloadThread extends Thread { plugin.getLogger().info("Loading offline players..."); OfflinePlayerHandler.updateOfflinePlayerList(getPlayerMap(true)); - TestFileHandler.savePlayerCount(OfflinePlayerHandler.getOfflinePlayerCount()); - TestFileHandler.saveTimeTaken(System.currentTimeMillis() - time, "onEnable"); + testFile.saveThreshold(OfflinePlayerHandler.getOfflinePlayerCount(), threshold); + testFile.saveTimeTaken(System.currentTimeMillis() - time, 1); plugin.getLogger().info("Amount of relevant players: " + OfflinePlayerHandler.getOfflinePlayerCount()); plugin.logTimeTaken("ReloadThread", "loading offline players", time); ThreadManager.recordCalcTime(System.currentTimeMillis() - time); @@ -80,18 +87,21 @@ public class ReloadThread extends Thread { ConcurrentHashMap playerMap = new ConcurrentHashMap<>(size); - ReloadAction task = new ReloadAction(offlinePlayers, config.whitelistOnly(), config.excludeBanned(), config.lastPlayedLimit(), playerMap); + ReloadAction task = new ReloadAction(threshold, offlinePlayers, config.whitelistOnly(), config.excludeBanned(), config.lastPlayedLimit(), playerMap); ForkJoinPool commonPool = ForkJoinPool.commonPool(); commonPool.invoke(task); - ConcurrentHashMap newPlayerMap = new ConcurrentHashMap<>(playerMap.size() * 11); + ConcurrentHashMap newPlayerMap = new ConcurrentHashMap<>(playerMap.size()); + /* for (int i = 0; i < 11; i++) { for (String key : playerMap.keySet()) { newPlayerMap.put(key + i, playerMap.get(key)); } } - //newPlayerMap.putAll(playerMap); + */ + + newPlayerMap.putAll(playerMap); return newPlayerMap; } } diff --git a/src/main/java/com/gmail/artemis/the/gr8/playerstats/statistic/StatThread.java b/src/main/java/com/gmail/artemis/the/gr8/playerstats/statistic/StatThread.java index 90a2c79..7ec0590 100644 --- a/src/main/java/com/gmail/artemis/the/gr8/playerstats/statistic/StatThread.java +++ b/src/main/java/com/gmail/artemis/the/gr8/playerstats/statistic/StatThread.java @@ -7,6 +7,7 @@ import com.gmail.artemis.the.gr8.playerstats.ThreadManager; import com.gmail.artemis.the.gr8.playerstats.filehandlers.ConfigHandler; import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler; import com.gmail.artemis.the.gr8.playerstats.utils.MessageFactory; +import com.google.common.collect.ImmutableList; import net.kyori.adventure.platform.bukkit.BukkitAudiences; import org.bukkit.Bukkit; import org.bukkit.OfflinePlayer; @@ -19,26 +20,31 @@ import java.util.ConcurrentModificationException; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ForkJoinPool; import java.util.stream.Collectors; public class StatThread extends Thread { + private final int threshold; private final StatRequest request; private final ReloadThread reloadThread; private final BukkitAudiences adventure; - private final ConfigHandler config; + private static ConfigHandler config; + private static TestFileHandler testFile; private final MessageFactory messageFactory; private final Main plugin; //constructor (called on thread creation) - public StatThread(StatRequest s, @Nullable ReloadThread r, BukkitAudiences b, ConfigHandler c, MessageFactory o, Main p) { + public StatThread(int threshold, StatRequest s, @Nullable ReloadThread r, BukkitAudiences b, ConfigHandler c, TestFileHandler t, MessageFactory o, Main p) { + this.threshold = threshold; request = s; reloadThread = r; adventure = b; config = c; + testFile = t; messageFactory = o; plugin = p; plugin.getLogger().info("StatThread created!"); @@ -84,11 +90,13 @@ public class StatThread extends Thread { adventure.sender(sender).sendMessage(messageFactory.formatTopStats( getTopStatistics(), statName, subStatEntry)); - TestFileHandler.saveTimeTaken(System.currentTimeMillis() - time, "top-stat"); + testFile.saveTimeTaken(System.currentTimeMillis() - time, 3); + testFile.logRunCount(false); plugin.logTimeTaken("StatThread", "calculating top stat", time); ThreadManager.recordCalcTime(System.currentTimeMillis() - time); } catch (ConcurrentModificationException e) { + testFile.logRunCount(true); adventure.sender(sender).sendMessage(messageFactory.unknownError()); } catch (Exception e) { sender.sendMessage(messageFactory.formatExceptions(e.toString())); @@ -101,7 +109,6 @@ public class StatThread extends Thread { adventure.sender(sender).sendMessage( messageFactory.formatPlayerStat( playerName, statName, subStatEntry, getStatistic())); - TestFileHandler.saveTimeTaken(System.currentTimeMillis() - time, "individual-stat"); plugin.logTimeTaken("StatThread", "calculating individual stat", time); } catch (Exception e) { @@ -125,8 +132,10 @@ public class StatThread extends Thread { //invokes a bunch of worker pool threads to divide and conquer (get the statistics for all players in the list) private LinkedHashMap getTopStatistics() throws ConcurrentModificationException { ConcurrentHashMap playerStats = new ConcurrentHashMap<>((int) (getOfflinePlayerCount() * 1.05)); - String[] playerNames = OfflinePlayerHandler.getOfflinePlayerNames().toArray(new String[0]); - TopStatAction task = new TopStatAction(playerNames, + //ConcurrentLinkedDeque playerNames = new ConcurrentLinkedDeque<>(OfflinePlayerHandler.getOfflinePlayerNames()); + //String[] playerNames = OfflinePlayerHandler.getOfflinePlayerNames().toArray(new String[0]); + ImmutableList playerNames = ImmutableList.copyOf(OfflinePlayerHandler.getOfflinePlayerNames()); + TopStatAction task = new TopStatAction(threshold, playerNames, request, playerStats); ForkJoinPool commonPool = ForkJoinPool.commonPool(); @@ -134,7 +143,17 @@ public class StatThread extends Thread { commonPool.invoke(task); } catch (ConcurrentModificationException e) { e.printStackTrace(); - throw new ConcurrentModificationException(e.toString()); + try { + if (!task.cancel(true)) { + plugin.getLogger().severe("Tried to cancel task, but failed. You might need to shut down the server and reboot"); + throw new ConcurrentModificationException(e.toString()); + } else { + plugin.getLogger().warning("Canceling task because of a ConcurrentModificationException..."); + } + } catch (ConcurrentModificationException ex) { + ex.printStackTrace(); + throw new ConcurrentModificationException(ex.toString()); + } } return playerStats.entrySet().stream() diff --git a/src/main/java/com/gmail/artemis/the/gr8/playerstats/statistic/TopStatAction.java b/src/main/java/com/gmail/artemis/the/gr8/playerstats/statistic/TopStatAction.java index 538e521..fe45df3 100644 --- a/src/main/java/com/gmail/artemis/the/gr8/playerstats/statistic/TopStatAction.java +++ b/src/main/java/com/gmail/artemis/the/gr8/playerstats/statistic/TopStatAction.java @@ -1,6 +1,7 @@ package com.gmail.artemis.the.gr8.playerstats.statistic; import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler; +import com.google.common.collect.ImmutableList; import org.bukkit.Bukkit; import org.bukkit.OfflinePlayer; @@ -11,9 +12,9 @@ import java.util.concurrent.RecursiveAction; public class TopStatAction extends RecursiveAction { - private static final int threshold = 11; + private final int threshold; - private final String[] playerNames; + private final ImmutableList playerNames; private final StatRequest request; private final ConcurrentHashMap playerStats; @@ -25,23 +26,22 @@ public class TopStatAction extends RecursiveAction { * @param playerStats the ConcurrentHashMap to put the results on */ - public TopStatAction(String[] playerNames, StatRequest statRequest, ConcurrentHashMap playerStats) { + public TopStatAction(int threshold, ImmutableList playerNames, StatRequest statRequest, ConcurrentHashMap playerStats) { + this.threshold = threshold; this.playerNames = playerNames; - request = statRequest; + + this.request = statRequest; this.playerStats = playerStats; } @Override protected void compute() { - if (playerNames.length < threshold) { + if (playerNames.size() < threshold) { getStatsDirectly(); } else { - Bukkit.getLogger().info("playerNames length: " + playerNames.length); - TopStatAction subTask1 = new TopStatAction(Arrays.copyOfRange(playerNames, 0, playerNames.length/2), - request, playerStats); - TopStatAction subTask2 = new TopStatAction(Arrays.copyOfRange(playerNames, playerNames.length/2, playerNames.length), - request, playerStats); + final TopStatAction subTask1 = new TopStatAction(threshold, playerNames.subList(0, playerNames.size()/2), request, playerStats); + final TopStatAction subTask2 = new TopStatAction(threshold, playerNames.subList(playerNames.size()/2, playerNames.size()), request, playerStats); //queue and compute all subtasks in the right order invokeAll(subTask1, subTask2); @@ -49,9 +49,10 @@ public class TopStatAction extends RecursiveAction { } private void getStatsDirectly() throws IllegalArgumentException, ConcurrentModificationException { - Bukkit.getLogger().info("ArrayCopy Length: " + playerNames.length); - for (String playerName : playerNames) { - try { + try { + Iterator iterator = playerNames.iterator(); + while (iterator.hasNext()) { + String playerName = iterator.next(); OfflinePlayer player = OfflinePlayerHandler.getOfflinePlayer(playerName); int statistic = 0; switch (request.getStatType()) { @@ -60,16 +61,17 @@ public class TopStatAction extends RecursiveAction { case BLOCK -> statistic = player.getStatistic(request.getStatEnum(), request.getBlock()); case ITEM -> statistic = player.getStatistic(request.getStatEnum(), request.getItem()); } - if (statistic > 0) { playerStats.put(playerName, statistic); } - } catch (IllegalArgumentException ignored) { - } - catch (ConcurrentModificationException e) { - Bukkit.getLogger().warning("A ConcurrentModificationException has occurred"); - throw new ConcurrentModificationException(e.toString()); } + } catch (NoSuchElementException e) { + e.printStackTrace(); + } catch (IllegalArgumentException ignored) { + } catch (ConcurrentModificationException e) { + Bukkit.getLogger().warning("A ConcurrentModificationException has occurred" + e.getCause()); + e.printStackTrace(); + throw new ConcurrentModificationException(e.toString()); } } } diff --git a/src/main/java/com/gmail/artemis/the/gr8/playerstats/utils/OfflinePlayerHandler.java b/src/main/java/com/gmail/artemis/the/gr8/playerstats/utils/OfflinePlayerHandler.java index 56e99c0..b7e55c7 100644 --- a/src/main/java/com/gmail/artemis/the/gr8/playerstats/utils/OfflinePlayerHandler.java +++ b/src/main/java/com/gmail/artemis/the/gr8/playerstats/utils/OfflinePlayerHandler.java @@ -9,6 +9,7 @@ import java.util.concurrent.ConcurrentHashMap; public class OfflinePlayerHandler { private static ConcurrentHashMap offlinePlayerUUIDs; + private static ArrayList playerNames; private OfflinePlayerHandler() { } @@ -23,7 +24,7 @@ public class OfflinePlayerHandler { } public static ArrayList getOfflinePlayerNames() { - return new ArrayList<>(offlinePlayerUUIDs.keySet()); + return playerNames; } /** @@ -32,6 +33,7 @@ public class OfflinePlayerHandler { */ public static void updateOfflinePlayerList(ConcurrentHashMap playerList) { offlinePlayerUUIDs = playerList; + playerNames = Collections.list(offlinePlayerUUIDs.keys()); } /**