diff --git a/src/main/java/org/mvplugins/multiverse/inventories/MultiverseInventories.java b/src/main/java/org/mvplugins/multiverse/inventories/MultiverseInventories.java index e0e3c620..513dee1f 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/MultiverseInventories.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/MultiverseInventories.java @@ -142,7 +142,7 @@ public void onDisable() { profileContainerStoreProvider.get().getStore(ContainerType.WORLD) .getContainer(world) .getPlayerData(player))); - profileDataSource.get().setLoadOnLogin(player.getUniqueId(), true); + profileDataSource.get().modifyGlobalProfile(player, profile -> profile.setLoadOnLogin(true)); } } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/CacheCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/CacheCommand.java index e8138afc..83f5a657 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/CacheCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/CacheCommand.java @@ -34,13 +34,16 @@ final class CacheCommand extends InventoriesCommand { void onCacheStatsCommand(MVCommandIssuer issuer) { Map stats = this.profileDataSource.getCacheStats(); for (Map.Entry entry : stats.entrySet()) { - issuer.sendInfo("Cache: " + entry.getKey()); - issuer.sendInfo(" hits count: " + entry.getValue().hitCount()); - issuer.sendInfo(" misses count: " + entry.getValue().missCount()); - issuer.sendInfo(" loads count: " + entry.getValue().loadCount()); - issuer.sendInfo(" avg load time: " + entry.getValue().averageLoadPenalty()); - issuer.sendInfo(" exceptions: " + entry.getValue().loadExceptionCount()); - issuer.sendInfo(" evictions: " + entry.getValue().evictionCount()); + issuer.sendMessage("Cache: " + entry.getKey()); + issuer.sendMessage(" hits count: " + entry.getValue().hitCount()); + issuer.sendMessage(" misses count: " + entry.getValue().missCount()); + issuer.sendMessage(" loads count: " + entry.getValue().loadCount()); + issuer.sendMessage(" exceptions: " + entry.getValue().loadExceptionCount()); + issuer.sendMessage(" evictions: " + entry.getValue().evictionCount()); + issuer.sendMessage(" hit rate: " + entry.getValue().hitRate() * 100 + "%"); + issuer.sendMessage(" miss rate: " + entry.getValue().missRate() * 100 + "%"); + issuer.sendMessage(" avg load penalty: " + entry.getValue().averageLoadPenalty() / 1000000 + "ms"); + issuer.sendMessage("--------"); } } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/dataimport/perworldinventory/PwiImportHelper.java b/src/main/java/org/mvplugins/multiverse/inventories/dataimport/perworldinventory/PwiImportHelper.java index 63563ef4..2666ea99 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/dataimport/perworldinventory/PwiImportHelper.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/dataimport/perworldinventory/PwiImportHelper.java @@ -205,11 +205,11 @@ private void saveMVDataForPlayer(Group group, OfflinePlayer offlinePlayer) throw private List getMVPlayerData( @NotNull OfflinePlayer offlinePlayer, @NotNull Group group, @NotNull GameMode gameMode) { List profiles = new ArrayList<>(); - profiles.add(profileDataSource.getPlayerData( - ContainerType.GROUP, group.getName(), ProfileTypes.forGameMode(gameMode), offlinePlayer.getUniqueId())); + profiles.add(profileDataSource.getPlayerData(org.mvplugins.multiverse.inventories.profile.ProfileKey + .create(ContainerType.GROUP, group.getName(), ProfileTypes.forGameMode(gameMode), offlinePlayer.getUniqueId()))); for (var worldName : group.getWorlds()) { - profiles.add(profileDataSource.getPlayerData( - ContainerType.WORLD, worldName, ProfileTypes.forGameMode(gameMode), offlinePlayer.getUniqueId())); + profiles.add(profileDataSource.getPlayerData(org.mvplugins.multiverse.inventories.profile.ProfileKey + .create(ContainerType.WORLD, worldName, ProfileTypes.forGameMode(gameMode), offlinePlayer.getUniqueId()))); } return profiles; } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/handleshare/ShareHandleListener.java b/src/main/java/org/mvplugins/multiverse/inventories/handleshare/ShareHandleListener.java index f72eaf73..c00ee02a 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/handleshare/ShareHandleListener.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/handleshare/ShareHandleListener.java @@ -151,7 +151,7 @@ void playerJoin(final PlayerJoinEvent event) { // Just in case AsyncPlayerPreLoginEvent was still the old name verifyCorrectPlayerName(player.getUniqueId(), player.getName()); - final GlobalProfile globalProfile = profileDataSource.getGlobalProfile(player.getName(), player.getUniqueId()); + final GlobalProfile globalProfile = profileDataSource.getGlobalProfile(player); final String world = globalProfile.getLastWorld(); if (config.usingLoggingSaveLoad() && globalProfile.shouldLoadOnLogin()) { ShareHandlingUpdater.updatePlayer(inventories, player, new PersistingProfile( @@ -167,7 +167,7 @@ void playerJoin(final PlayerJoinEvent event) { } private void verifyCorrectPlayerName(UUID uuid, String name) { - profileDataSource.getExistingGlobalProfile(name, uuid).peek(globalProfile -> { + profileDataSource.getExistingGlobalProfile(uuid, name).peek(globalProfile -> { if (globalProfile.getLastKnownName().equals(name)) { return; } @@ -196,7 +196,8 @@ private void verifyCorrectPlayerName(UUID uuid, String name) { void playerQuit(final PlayerQuitEvent event) { final Player player = event.getPlayer(); final String world = event.getPlayer().getWorld().getName(); - profileDataSource.updateLastWorld(player.getUniqueId(), world); + GlobalProfile globalProfile = profileDataSource.getGlobalProfile(player); + globalProfile.setLastWorld(world); if (config.usingLoggingSaveLoad()) { ShareHandlingUpdater.updateProfile(inventories, player, new PersistingProfile( Sharables.allOf(), @@ -204,8 +205,9 @@ void playerQuit(final PlayerQuitEvent event) { .getContainer(world) .getPlayerData(player) )); - profileDataSource.setLoadOnLogin(player.getUniqueId(), true); + globalProfile.setLoadOnLogin(true); } + profileDataSource.updateGlobalProfile(globalProfile); SingleShareWriter.of(this.inventories, player, Sharables.LAST_LOCATION).write(player.getLocation().clone()); } @@ -261,7 +263,7 @@ void playerChangedWorld(PlayerChangedWorldEvent event) { long startTime = System.nanoTime(); new WorldChangeShareHandler(this.inventories, player, fromWorld.getName(), toWorld.getName()).handleSharing(); - profileDataSource.updateLastWorld(player.getUniqueId(), toWorld.getName()); + profileDataSource.modifyGlobalProfile(player, profile -> profile.setLastWorld(toWorld.getName())); Logging.finest("WorldChangeShareHandler took " + (System.nanoTime() - startTime) / 1000000 + " ms."); } @@ -323,7 +325,7 @@ void playerRespawn(PlayerRespawnEvent event) { () -> verifyCorrectWorld( player, player.getWorld().getName(), - profileDataSource.getGlobalProfile(player.getName(), player.getUniqueId())), + profileDataSource.getGlobalProfile(player)), 2L); } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/FlatFileProfileDataSource.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/FlatFileProfileDataSource.java index f617ba05..f35d266b 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/FlatFileProfileDataSource.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/FlatFileProfileDataSource.java @@ -9,11 +9,11 @@ import net.minidev.json.parser.JSONParser; import net.minidev.json.parser.ParseException; import org.bukkit.OfflinePlayer; -import org.bukkit.configuration.InvalidConfigurationException; import org.jetbrains.annotations.NotNull; import org.jvnet.hk2.annotations.Service; import org.mvplugins.multiverse.external.jakarta.inject.Inject; import org.mvplugins.multiverse.external.vavr.control.Option; +import org.mvplugins.multiverse.external.vavr.control.Try; import org.mvplugins.multiverse.inventories.MultiverseInventories; import org.mvplugins.multiverse.inventories.share.ProfileEntry; import org.mvplugins.multiverse.inventories.share.Sharable; @@ -29,8 +29,13 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Set; import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import java.util.function.Predicate; import java.util.logging.Level; @@ -97,7 +102,17 @@ final class FlatFileProfileDataSource implements ProfileDataSource { } /** - * Retrieves the data file for a player based on a given world/group name, creating it if necessary. + * Retrieves the data file for a player based on a given world/group name. + * + * @param profileKey The profile target to get the file + * @return The data file for a player. + */ + private File getPlayerFile(ProfileKey profileKey) { + return getPlayerFile(profileKey.getContainerType(), profileKey.getDataName(), profileKey.getPlayerName()); + } + + /** + * Retrieves the data file for a player based on a given world/group name. * * @param type Indicates whether data is for group or world. * @param dataName The name of the group or world. @@ -118,53 +133,56 @@ private File getProfileContainerFolder(ContainerType type, String folderName) { default -> new File(this.worldFolder, folderName); }; - if (!folder.exists()) { - folder.mkdirs(); + if (!folder.exists() && !folder.mkdirs()) { + Logging.severe("Could not create profile container folder!"); } return folder; } + private FileConfiguration parseToConfiguration(File file) { + JsonConfiguration jsonConfiguration = new JsonConfiguration(); + jsonConfiguration.options().continueOnSerializationError(true); + Try.run(() -> jsonConfiguration.load(file)).getOrElseThrow(e -> { + Logging.severe("Could not load file: " + file); + throw new RuntimeException(e); + }); + return jsonConfiguration; + } + + private FileConfiguration getOrLoadProfileFile(ProfileKey profileKey, File playerFile) { + ProfileKey fileProfileKey = profileKey.forProfileType(null); + return Try.of(() -> + configCache.get(fileProfileKey, () -> playerFile.exists() + ? parseToConfiguration(playerFile) + : new JsonConfiguration()) + ).getOrElseThrow(e -> { + Logging.severe("Could not load profile data for player: " + fileProfileKey); + return new RuntimeException(e); + }); + } + /** * {@inheritDoc} */ @Override - public void updatePlayerData(PlayerProfile playerProfile) { - playerProfileIO.queueAction(() -> processProfileWrite(playerProfile.clone())); + public Future updatePlayerData(PlayerProfile playerProfile) { + return playerProfileIO.queueAction(() -> processUpdatePlayerData(playerProfile.clone())); } - private void processProfileWrite(PlayerProfile playerProfile) { - File playerFile = getPlayerFile( - playerProfile.getContainerType(), - playerProfile.getContainerName(), - playerProfile.getPlayer().getName() - ); - try { - ProfileKey fileProfileKey = ProfileKey.create( - playerProfile.getContainerType(), - playerProfile.getContainerName(), - null, - playerProfile.getPlayer().getUniqueId(), - playerProfile.getPlayer().getName()); - FileConfiguration playerData = configCache.getIfPresent(fileProfileKey); - if (playerData == null) { - playerData = playerFile.exists() - ? playerProfileIO.getConfigHandleNow(playerFile) - : new JsonConfiguration(); - configCache.put(fileProfileKey, playerData); - } - Map serializedData = serializePlayerProfile(playerProfile); - if (serializedData.isEmpty()) { - return; - } - playerData.createSection(playerProfile.getProfileType().getName(), serializedData); - playerData.save(playerFile); - } catch (IOException e) { + private void processUpdatePlayerData(PlayerProfile playerProfile) { + ProfileKey profileKey = ProfileKey.fromPlayerProfile(playerProfile); + File playerFile = getPlayerFile(profileKey); + FileConfiguration playerData = getOrLoadProfileFile(profileKey, playerFile); + Map serializedData = serializePlayerProfile(playerProfile); + if (serializedData.isEmpty()) { + return; + } + playerData.createSection(playerProfile.getProfileType().getName(), serializedData); + Try.run(() -> playerData.save(playerFile)).onFailure(e -> { Logging.severe("Could not save data for player: " + playerProfile.getPlayer().getName() + " for " + playerProfile.getContainerType() + ": " + playerProfile.getContainerName()); Logging.severe(e.getMessage()); - } catch (Exception e) { - Logging.severe("Unknown error while attempting to write profile data.", e); - } + }); } private Map serializePlayerProfile(PlayerProfile playerProfile) { @@ -204,38 +222,28 @@ private Map serializePlayerProfile(PlayerProfile playerProfile) * {@inheritDoc} */ @Override - public PlayerProfile getPlayerData(ContainerType containerType, String dataName, ProfileType profileType, UUID playerUUID) { - return getPlayerData(ProfileKey.create(containerType, dataName, profileType, playerUUID)); - } - - private PlayerProfile getPlayerData(ProfileKey key) { - PlayerProfile cached = profileCache.getIfPresent(key); - if (cached != null) { - return cached; - } - File playerFile = getPlayerFile(key.getContainerType(), key.getDataName(), key.getPlayerName()); - if (!playerFile.exists()) { - PlayerProfile playerProfile = PlayerProfile.createPlayerProfile(key.getContainerType(), key.getDataName(), - key.getProfileType(), Bukkit.getOfflinePlayer(key.getPlayerUUID())); - profileCache.put(key, playerProfile); - return playerProfile; + public PlayerProfile getPlayerData(ProfileKey key) { + try { + return profileCache.get(key, () -> { + File playerFile = getPlayerFile(key.getContainerType(), key.getDataName(), key.getPlayerName()); + if (!playerFile.exists()) { + return PlayerProfile.createPlayerProfile(key.getContainerType(), key.getDataName(), + key.getProfileType(), Bukkit.getOfflinePlayer(key.getPlayerUUID())); + } + return playerProfileIO.waitForData(() -> getPlayerDataFromDisk(key, playerFile)); + }); + } catch (ExecutionException e) { + Logging.severe("Could not get data for player: " + key.getPlayerName() + + " for " + key.getContainerType().toString() + ": " + key.getDataName()); + throw new RuntimeException(e); } - return playerProfileIO.waitForData(() -> loadPlayerData(key, playerFile)); } - private PlayerProfile loadPlayerData(ProfileKey key, File playerFile) { + private PlayerProfile getPlayerDataFromDisk(ProfileKey key, File playerFile) { + FileConfiguration playerData = getOrLoadProfileFile(key, playerFile); + // Migrate from none profile-type data - ProfileKey fileProfileKey = key.forProfileType(null); - FileConfiguration playerData = configCache.getIfPresent(fileProfileKey); - if (playerData == null) { - try { - playerData = playerProfileIO.getConfigHandleNow(playerFile); - } catch (IOException | InvalidConfigurationException e) { - throw new RuntimeException(e); - } - configCache.put(fileProfileKey, playerData); - } - if (convertConfig(playerData)) { + if (migrateToProfileType(playerData)) { try { playerData.save(playerFile); } catch (IOException e) { @@ -249,9 +257,35 @@ private PlayerProfile loadPlayerData(ProfileKey key, File playerFile) { if (section == null) { section = playerData.createSection(key.getProfileType().getName()); } - PlayerProfile playerProfile = deserializePlayerProfile(key, convertSection(section)); - profileCache.put(key, playerProfile); - return playerProfile; + return deserializePlayerProfile(key, convertSection(section)); + } + + @Deprecated + private boolean migrateToProfileType(FileConfiguration config) { + ConfigurationSection section = config.getConfigurationSection(DataStrings.PLAYER_DATA); + if (section == null) { + return false; + } + config.set(ProfileTypes.SURVIVAL.getName(), section); + config.set(ProfileTypes.CREATIVE.getName(), section); + config.set(ProfileTypes.ADVENTURE.getName(), section); + config.set(DataStrings.PLAYER_DATA, null); + Logging.finer("Migrated old player data to new multi-profile format"); + return true; + } + + private Map convertSection(ConfigurationSection section) { + Set keys = section.getKeys(false); + Map resultMap = new HashMap<>(keys.size()); + for (String key : keys) { + Object obj = section.get(key); + if (obj instanceof ConfigurationSection) { + resultMap.put(key, convertSection((ConfigurationSection) obj)); + } else { + resultMap.put(key, obj); + } + } + return resultMap; } private PlayerProfile deserializePlayerProfile(ProfileKey pKey, Map playerData) { @@ -327,111 +361,117 @@ private void parseJsonPlayerStatsIntoProfile(String stats, PlayerProfile profile parsePlayerStatsIntoProfile(jsonStats, profile); } - @Deprecated - private boolean convertConfig(FileConfiguration config) { - ConfigurationSection section = config.getConfigurationSection(DataStrings.PLAYER_DATA); - if (section == null) { - return false; - } - config.set(ProfileTypes.SURVIVAL.getName(), section); - config.set(ProfileTypes.CREATIVE.getName(), section); - config.set(ProfileTypes.ADVENTURE.getName(), section); - config.set(DataStrings.PLAYER_DATA, null); - Logging.finer("Migrated old player data to new multi-profile format"); - return true; - } - /** * {@inheritDoc} */ @Override - public boolean removePlayerData(ContainerType containerType, String dataName, ProfileType profileType, UUID playerUUID) { - ProfileKey profileKey = ProfileKey.create(containerType, dataName, profileType, playerUUID); + public Future removePlayerData(ProfileKey profileKey) { if (profileKey.getProfileType() == null) { - try { - File playerFile = getPlayerFile(containerType, dataName, profileKey.getPlayerName()); - configCache.invalidate(profileKey); - profileCache.invalidateAll(Sets.filter( - profileCache.asMap().keySet(), - key -> key.getPlayerUUID().equals(playerUUID) - && key.getContainerType().equals(containerType) - && key.getDataName().equals(dataName) - )); - playerProfileIO.queueAction(playerFile::delete); - } catch (Exception ignore) { + clearProfileCache(key -> key.getPlayerUUID().equals(profileKey.getPlayerUUID()) + && key.getContainerType().equals(profileKey.getContainerType()) + && key.getDataName().equals(profileKey.getDataName())); + File playerFile = getPlayerFile(profileKey); + if (!playerFile.exists()) { Logging.warning("Attempted to delete file that did not exist for player " + profileKey.getPlayerName() - + " in " + containerType.name().toLowerCase() + " " + dataName); - return false; + + " in " + profileKey.getContainerType() + " " + profileKey.getDataName()); + return CompletableFuture.completedFuture(null); } + return playerProfileIO.queueAction(playerFile::delete); } + profileCache.invalidate(profileKey); + return playerProfileIO.queueAction(() -> processRemovePlayerData(profileKey)); + } + + private void processRemovePlayerData(ProfileKey profileKey) { try { - profileCache.invalidate(profileKey); - playerProfileIO.queueAction(() -> processProfileRemove(profileKey)); - } catch (Exception e) { + File playerFile = getPlayerFile(profileKey.getContainerType(), profileKey.getDataName(), profileKey.getPlayerName()); + FileConfiguration playerData = getOrLoadProfileFile(profileKey, playerFile); + playerData.set(profileKey.getProfileType().getName(), null); + playerData.save(playerFile); + } catch (IOException e) { Logging.severe("Could not delete data for player: " + profileKey.getPlayerName() - + " for " + containerType.toString() + ": " + dataName); + + " for " + profileKey.getContainerType() + ": " + profileKey.getDataName()); Logging.severe(e.getMessage()); - return false; } - return true; } - private void processProfileRemove(ProfileKey profileKey) { - try { - File playerFile = getPlayerFile(profileKey.getContainerType(), profileKey.getDataName(), profileKey.getPlayerName()); - ProfileKey fileProfileKey = profileKey.forProfileType(null); - FileConfiguration playerData = configCache.getIfPresent(fileProfileKey); - if (playerData == null) { - if (!playerFile.exists()) { - return; - } - playerData = playerProfileIO.getConfigHandleNow(playerFile); - configCache.put(fileProfileKey, playerData); - } - playerData.set(profileKey.getProfileType().getName(), null); - FileConfiguration finalPlayerData = playerData; - finalPlayerData.save(playerFile); - } catch (IOException | InvalidConfigurationException e) { - throw new RuntimeException(e); + /** + * {@inheritDoc} + */ + @Override + public void migratePlayerData(String oldName, String newName, UUID uuid) throws IOException { + clearPlayerCache(uuid); + + File[] worldFolders = worldFolder.listFiles(File::isDirectory); + if (worldFolders == null) { + throw new IOException("Could not enumerate world folders"); + } + File[] groupFolders = groupFolder.listFiles(File::isDirectory); + if (groupFolders == null) { + throw new IOException("Could not enumerate group folders"); } + + migrateForContainerType(worldFolders, ContainerType.WORLD, oldName, newName); + migrateForContainerType(groupFolders, ContainerType.GROUP, oldName, newName); } - private Map convertSection(ConfigurationSection section) { - Map resultMap = new HashMap(); - for (String key : section.getKeys(false)) { - Object obj = section.get(key); - if (obj instanceof ConfigurationSection) { - resultMap.put(key, convertSection((ConfigurationSection) obj)); - } else { - resultMap.put(key, obj); + private void migrateForContainerType(File[] folders, ContainerType containerType, String oldName, String newName) { + for (File folder : folders) { + File oldNameFile = getPlayerFile(containerType, folder.getName(), oldName); + File newNameFile = getPlayerFile(containerType, folder.getName(), newName); + if (!oldNameFile.exists()) { + Logging.fine("No old data for player %s in %s %s to migrate.", + oldName, containerType.name(), folder.getName()); + continue; + } + if (newNameFile.exists()) { + Logging.warning("Data already exists for player %s in %s %s. Not migrating.", + newName, containerType.name(), folder.getName()); + continue; + } + if (!oldNameFile.renameTo(newNameFile)) { + Logging.warning("Could not rename old data file for player %s in %s %s to %s.", + oldName, containerType.name(), folder.getName(), newName); + continue; } + Logging.fine("Migrated data for player %s in %s %s to %s.", + oldName, containerType.name(), folder.getName(), newName); } - return resultMap; } + @NotNull @Override public GlobalProfile getGlobalProfile(UUID playerUUID) { return getGlobalProfile(Bukkit.getOfflinePlayer(playerUUID)); } + @NotNull @Override public GlobalProfile getGlobalProfile(OfflinePlayer player) { - return getGlobalProfile(player.getName(), player.getUniqueId()); + return getGlobalProfile(player.getUniqueId(), player.getName()); } + @NotNull @Override - public @NotNull GlobalProfile getGlobalProfile(String playerName, UUID playerUUID) { - return getExistingGlobalProfile(playerName, playerUUID) - .getOrElse(() -> GlobalProfile.createGlobalProfile(playerName, playerUUID)); + public GlobalProfile getGlobalProfile(UUID playerUUID, String playerName) { + try { + return globalProfileCache.get(playerUUID, () -> getGlobalProfileFromDisk(playerUUID, playerName)); + } catch (ExecutionException e) { + Logging.severe("Unable to get global profile for player: " + playerName); + throw new RuntimeException(e); + } } @Override - public @NotNull Option getExistingGlobalProfile(String playerName, UUID playerUUID) { - GlobalProfile cached = globalProfileCache.getIfPresent(playerUUID); - if (cached != null) { - return Option.of(cached); + public @NotNull Option getExistingGlobalProfile(UUID playerUUID, String playerName) { + File uuidFile = getGlobalFile(playerUUID.toString()); + if (!uuidFile.exists()) { + return Option.none(); } + return Option.of(getGlobalProfile(playerUUID, playerName)); + } + private GlobalProfile getGlobalProfileFromDisk(UUID playerUUID, String playerName) { // Migrate from player name to uuid profile file File legacyFile = getGlobalFile(playerName); if (legacyFile.exists() && !migrateGlobalProfileToUUID(legacyFile, playerUUID)) { @@ -440,12 +480,10 @@ public GlobalProfile getGlobalProfile(OfflinePlayer player) { // Load from existing profile file File uuidFile = getGlobalFile(playerUUID.toString()); - if (uuidFile.exists()) { - GlobalProfile globalProfile = loadGlobalProfile(uuidFile, playerName, playerUUID); - globalProfileCache.put(playerUUID, globalProfile); - return Option.of(globalProfile); + if (!uuidFile.exists()) { + return GlobalProfile.createGlobalProfile(playerUUID, playerName); } - return Option.none(); + return loadGlobalProfile(uuidFile, playerName, playerUUID); } private boolean migrateGlobalProfileToUUID(File legacyFile, UUID playerUUID) { @@ -453,7 +491,7 @@ private boolean migrateGlobalProfileToUUID(File legacyFile, UUID playerUUID) { } private GlobalProfile loadGlobalProfile(File globalFile, String playerName, UUID playerUUID) { - FileConfiguration playerData = globalProfileIO.waitForConfigHandle(globalFile); + FileConfiguration playerData = globalProfileIO.waitForData(() -> parseToConfiguration(globalFile)); ConfigurationSection section = playerData.getConfigurationSection(DataStrings.PLAYER_DATA); if (section == null) { section = playerData.createSection(DataStrings.PLAYER_DATA); @@ -461,12 +499,25 @@ private GlobalProfile loadGlobalProfile(File globalFile, String playerName, UUID return GlobalProfile.deserialize(playerName, playerUUID, section); } + public Future modifyGlobalProfile(UUID playerUUID, Consumer consumer) { + return modifyGlobalProfile(getGlobalProfile(playerUUID), consumer); + } + + public Future modifyGlobalProfile(OfflinePlayer offlinePlayer, Consumer consumer) { + return modifyGlobalProfile(getGlobalProfile(offlinePlayer), consumer); + } + + private Future modifyGlobalProfile(GlobalProfile globalProfile, Consumer consumer) { + consumer.accept(globalProfile); + return updateGlobalProfile(globalProfile); + } + /** * {@inheritDoc} */ @Override - public void updateGlobalProfile(GlobalProfile globalProfile) { - globalProfileIO.queueAction(() -> processGlobalProfileWrite(globalProfile)); + public Future updateGlobalProfile(GlobalProfile globalProfile) { + return globalProfileIO.queueAction(() -> processGlobalProfileWrite(globalProfile)); } private void processGlobalProfileWrite(GlobalProfile globalProfile) { @@ -491,61 +542,6 @@ private File getGlobalFile(String fileName) { return new File(playerFolder, fileName + JSON); } - @Override - public void updateLastWorld(UUID playerUUID, String worldName) { - GlobalProfile globalProfile = getGlobalProfile(playerUUID); - globalProfile.setLastWorld(worldName); - updateGlobalProfile(globalProfile); - } - - @Override - public void setLoadOnLogin(final UUID playerUUID, final boolean loadOnLogin) { - final GlobalProfile globalProfile = getGlobalProfile(playerUUID); - globalProfile.setLoadOnLogin(loadOnLogin); - updateGlobalProfile(globalProfile); - } - - @Override - public void migratePlayerData(String oldName, String newName, UUID uuid) throws IOException { - clearPlayerCache(uuid); - - File[] worldFolders = worldFolder.listFiles(File::isDirectory); - if (worldFolders == null) { - throw new IOException("Could not enumerate world folders"); - } - File[] groupFolders = groupFolder.listFiles(File::isDirectory); - if (groupFolders == null) { - throw new IOException("Could not enumerate group folders"); - } - - migrateForContainerType(worldFolders, ContainerType.WORLD, oldName, newName); - migrateForContainerType(groupFolders, ContainerType.GROUP, oldName, newName); - } - - private void migrateForContainerType(File[] folders, ContainerType containerType, String oldName, String newName) throws IOException { - for (File folder : folders) { - File oldNameFile = getPlayerFile(containerType, folder.getName(), oldName); - File newNameFile = getPlayerFile(containerType, folder.getName(), newName); - if (!oldNameFile.exists()) { - Logging.fine("No old data for player %s in %s %s to migrate.", - oldName, containerType.name(), folder.getName()); - continue; - } - if (newNameFile.exists()) { - Logging.warning("Data already exists for player %s in %s %s. Not migrating.", - newName, containerType.name(), folder.getName()); - continue; - } - if (!oldNameFile.renameTo(newNameFile)) { - Logging.warning("Could not rename old data file for player %s in %s %s to %s.", - oldName, containerType.name(), folder.getName(), newName); - continue; - } - Logging.fine("Migrated data for player %s in %s %s to %s.", - oldName, containerType.name(), folder.getName(), newName); - } - } - void clearPlayerCache(UUID playerUUID) { clearProfileCache(key -> key.getPlayerUUID().equals(playerUUID)); } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/GlobalProfile.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/GlobalProfile.java index fb80d856..445b2d2a 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/GlobalProfile.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/GlobalProfile.java @@ -26,11 +26,11 @@ static GlobalProfile createGlobalProfile(OfflinePlayer player) { /** * Creates a global profile object for the given player with default values. * - * @param playerName the player to create the profile object for. * @param playerUUID the UUID of the player to create the profile for. + * @param playerName the player to create the profile object for. * @return a new GlobalProfile for the given player. */ - static GlobalProfile createGlobalProfile(String playerName, UUID playerUUID) { + static GlobalProfile createGlobalProfile(UUID playerUUID, String playerName) { return new GlobalProfile(playerName, playerUUID); } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileDataSource.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileDataSource.java index 3f05614d..e9e9d748 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileDataSource.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileDataSource.java @@ -5,11 +5,12 @@ import org.jetbrains.annotations.NotNull; import org.jvnet.hk2.annotations.Contract; import org.mvplugins.multiverse.external.vavr.control.Option; -import org.mvplugins.multiverse.inventories.profile.container.ContainerType; import java.io.IOException; import java.util.Map; import java.util.UUID; +import java.util.concurrent.Future; +import java.util.function.Consumer; import java.util.function.Predicate; /** @@ -24,31 +25,34 @@ public sealed interface ProfileDataSource permits FlatFileProfileDataSource { * * @param playerProfile The profile for the player that is being updated. */ - void updatePlayerData(PlayerProfile playerProfile); + Future updatePlayerData(PlayerProfile playerProfile); /** * Retrieves a PlayerProfile from the data source. * - * @param containerType The type of container this profile is part of, world or group. - * @param dataName World/Group to retrieve from. - * @param profileType The type of profile to load data for, typically based on game mode. - * @param playerUUID UUID of the player to retrieve for. + * @param profileKey The key of the profile to retrieve. * @return The player as returned from data. If no data was found, a new PlayerProfile will be * created. */ - PlayerProfile getPlayerData(ContainerType containerType, String dataName, ProfileType profileType, UUID playerUUID); + PlayerProfile getPlayerData(ProfileKey profileKey); /** * Removes the persisted data for a player for a specific profile. * - * @param containerType The type of container this profile is part of, world or group. - * @param dataName The name of the world/group the player's data is associated with. - * @param profileType The type of profile we're removing, as per {@link ProfileType}. If null, this will remove - * remove all profile types. - * @param playerUUID The UUID of the player whose data is being removed. + * @param profileKey The key of the profile to remove. * @return True if successfully removed. */ - boolean removePlayerData(ContainerType containerType, String dataName, ProfileType profileType, UUID playerUUID); + Future removePlayerData(ProfileKey profileKey); + + /** + * Copies all the data belonging to oldName to newName and removes the old data. + * + * @param oldName the previous name of the player. + * @param newName the new name of the player. + * @param playerUUID the UUID of the player. + * @throws IOException Thrown if something goes wrong while migrating the files. + */ + void migratePlayerData(String oldName, String newName, UUID playerUUID) throws IOException; /** * Retrieves the global profile for a player which contains meta-data for the player. @@ -56,7 +60,7 @@ public sealed interface ProfileDataSource permits FlatFileProfileDataSource { * @param playerUUID The UUID of the player. * @return The global profile for the specified player. */ - GlobalProfile getGlobalProfile(UUID playerUUID); + @NotNull GlobalProfile getGlobalProfile(UUID playerUUID); /** * Retrieves the global profile for a player which contains meta-data for the player. @@ -64,59 +68,37 @@ public sealed interface ProfileDataSource permits FlatFileProfileDataSource { * @param player The player. * @return The global profile for the specified player. */ - GlobalProfile getGlobalProfile(OfflinePlayer player); + @NotNull GlobalProfile getGlobalProfile(OfflinePlayer player); /** * Retrieves the global profile for a player which contains meta-data for the player. * Creates the profile if it doesn't exist. * - * @param playerName The name of the player. * @param playerUUID The UUID of the player. + * @param playerName The name of the player. * @return The global profile for the specified player. */ - @NotNull GlobalProfile getGlobalProfile(String playerName, UUID playerUUID); + @NotNull GlobalProfile getGlobalProfile(UUID playerUUID, String playerName); /** * Retrieves the global profile for a player which contains meta-data for the player if it exists. * - * @param playerName The name of the player. - * @param playerUUID The UUID of the player. + * @param playerUUID The UUID of the player. + * @param playerName The name of the player. * @return The global profile for the specified player or empty if it doesn't exist. */ - @NotNull Option getExistingGlobalProfile(String playerName, UUID playerUUID); - - /** - * Update the file for a player's global profile. - * - * @param globalProfile The GlobalProfile object to update the file for. - */ - void updateGlobalProfile(GlobalProfile globalProfile); + @NotNull Option getExistingGlobalProfile(UUID playerUUID, String playerName); - /** - * A convenience method to update the GlobalProfile of a player with a specified world. - * - * @param playerUUID The player whose global profile this will update. - * @param worldName The world to update the global profile with. - */ - void updateLastWorld(UUID playerUUID, String worldName); + Future modifyGlobalProfile(UUID playerUUID, Consumer consumer); - /** - * A convenience method for setting whether player data should be loaded on login for the specified player. - * - * @param playerUUID The player whose data should be loaded. - * @param loadOnLogin Whether or not to load on login. - */ - void setLoadOnLogin(UUID playerUUID, boolean loadOnLogin); + Future modifyGlobalProfile(OfflinePlayer offlinePlayer, Consumer consumer); /** - * Copies all the data belonging to oldName to newName and removes the old data. + * Update the file for a player's global profile. * - * @param oldName the previous name of the player. - * @param newName the new name of the player. - * @param playerUUID the UUID of the player. - * @throws IOException Thrown if something goes wrong while migrating the files. + * @param globalProfile The GlobalProfile object to update the file for. */ - void migratePlayerData(String oldName, String newName, UUID playerUUID) throws IOException; + Future updateGlobalProfile(GlobalProfile globalProfile); /** * Clears a single profile in cache. diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileFileIO.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileFileIO.java index 8af2ab26..e4bf520e 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileFileIO.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileFileIO.java @@ -1,11 +1,5 @@ package org.mvplugins.multiverse.inventories.profile; -import com.dumptruckman.bukkit.configuration.json.JsonConfiguration; -import org.bukkit.configuration.InvalidConfigurationException; -import org.bukkit.configuration.file.FileConfiguration; - -import java.io.File; -import java.io.IOException; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -13,7 +7,6 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.function.Supplier; final class ProfileFileIO { @@ -23,46 +16,17 @@ final class ProfileFileIO { fileIOExecutorService = Executors.newSingleThreadExecutor(); } - FileConfiguration waitForConfigHandle(File file) { - Future future = fileIOExecutorService.submit(new ConfigLoader(file)); - try { - return future.get(10, TimeUnit.SECONDS); - } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw new RuntimeException(e); - } - } - - FileConfiguration getConfigHandleNow(File file) throws IOException, InvalidConfigurationException { - JsonConfiguration jsonConfiguration = new JsonConfiguration(); - jsonConfiguration.options().continueOnSerializationError(true); - jsonConfiguration.load(file); - return jsonConfiguration; - } - - private class ConfigLoader implements Callable { - private final File file; - - private ConfigLoader(File file) { - this.file = file; - } - - @Override - public FileConfiguration call() throws Exception { - return getConfigHandleNow(file); - } - } - - void queueAction(Runnable action) { - fileIOExecutorService.submit(action); + Future queueAction(Runnable action) { + return (Future) fileIOExecutorService.submit(action); } - Future queueSupplier(Supplier supplier) { - return fileIOExecutorService.submit(supplier::get); + Future queueCallable(Callable callable) { + return fileIOExecutorService.submit(callable); } - T waitForData(Supplier supplier) { + T waitForData(Callable callable) { try { - return queueSupplier(supplier).get(10, TimeUnit.SECONDS); + return queueCallable(callable).get(10, TimeUnit.SECONDS); } catch (InterruptedException | ExecutionException | TimeoutException e) { throw new RuntimeException(e); } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileKey.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileKey.java index ee01e459..81d8c3b9 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileKey.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileKey.java @@ -1,6 +1,7 @@ package org.mvplugins.multiverse.inventories.profile; import com.google.common.base.Objects; +import org.bukkit.OfflinePlayer; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.mvplugins.multiverse.inventories.profile.container.ContainerType; @@ -19,6 +20,14 @@ public static ProfileKey create( return new ProfileKey(containerType, dataName, profileType, playerUUID, playerName); } + public static ProfileKey create( + ContainerType containerType, + String dataName, + ProfileType profileType, + OfflinePlayer offlinePlayer) { + return new ProfileKey(containerType, dataName, profileType, offlinePlayer.getUniqueId(), offlinePlayer.getName()); + } + public static ProfileKey create( ContainerType containerType, String dataName, diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/container/ProfileContainer.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/container/ProfileContainer.java index 78fd90f8..748b9748 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/container/ProfileContainer.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/container/ProfileContainer.java @@ -4,6 +4,7 @@ import org.mvplugins.multiverse.inventories.MultiverseInventories; import org.mvplugins.multiverse.inventories.config.InventoriesConfig; import org.mvplugins.multiverse.inventories.profile.ProfileDataSource; +import org.mvplugins.multiverse.inventories.profile.ProfileKey; import org.mvplugins.multiverse.inventories.profile.ProfileTypes; import org.mvplugins.multiverse.inventories.profile.PlayerProfile; import org.mvplugins.multiverse.inventories.profile.ProfileType; @@ -75,8 +76,11 @@ public PlayerProfile getPlayerData(ProfileType profileType, OfflinePlayer player Map profileMap = this.getPlayerData(player.getName()); PlayerProfile playerProfile = profileMap.get(profileType); if (playerProfile == null) { - playerProfile = profileDataSource.getPlayerData(getContainerType(), - getContainerName(), profileType, player.getUniqueId()); + playerProfile = profileDataSource.getPlayerData(ProfileKey.create( + getContainerType(), + getContainerName(), + profileType, + player)); Logging.finer("[%s - %s - %s - %s] not cached, loading from disk...", profileType, getContainerType(), playerProfile.getContainerName(), player.getName()); profileMap.put(profileType, playerProfile); @@ -100,7 +104,11 @@ public void addPlayerData(PlayerProfile playerProfile) { */ public void removeAllPlayerData(OfflinePlayer player) { this.getPlayerData(player.getName()).clear(); - profileDataSource.removePlayerData(getContainerType(), getContainerName(), null, player.getUniqueId()); + profileDataSource.removePlayerData(ProfileKey.create( + getContainerType(), + getContainerName(), + null, + player.getUniqueId())); } /** @@ -111,7 +119,11 @@ public void removeAllPlayerData(OfflinePlayer player) { */ public void removePlayerData(ProfileType profileType, OfflinePlayer player) { this.getPlayerData(player.getName()).remove(profileType); - profileDataSource.removePlayerData(getContainerType(), getContainerName(), profileType, player.getUniqueId()); + profileDataSource.removePlayerData(ProfileKey.create( + getContainerType(), + getContainerName(), + profileType, + player.getUniqueId())); } /** diff --git a/src/test/java/org/mvplugins/multiverse/inventories/handleshare/ShareHandlingUpdaterTest.kt b/src/test/java/org/mvplugins/multiverse/inventories/handleshare/ShareHandlingUpdaterTest.kt index 7e7718bf..45002e5d 100644 --- a/src/test/java/org/mvplugins/multiverse/inventories/handleshare/ShareHandlingUpdaterTest.kt +++ b/src/test/java/org/mvplugins/multiverse/inventories/handleshare/ShareHandlingUpdaterTest.kt @@ -3,6 +3,7 @@ package org.mvplugins.multiverse.inventories.handleshare import org.mockbukkit.mockbukkit.entity.PlayerMock import org.mvplugins.multiverse.inventories.TestWithMockBukkit import org.mvplugins.multiverse.inventories.profile.ProfileDataSource +import org.mvplugins.multiverse.inventories.profile.ProfileKey import org.mvplugins.multiverse.inventories.profile.ProfileTypes import org.mvplugins.multiverse.inventories.profile.container.ContainerType import org.mvplugins.multiverse.inventories.share.Sharables @@ -28,7 +29,8 @@ class ShareHandlingUpdaterTest : TestWithMockBukkit() { player.health = 4.4 player.maxHealth = 15.1 - val playerProfile = profileDataSource.getPlayerData(ContainerType.WORLD, "world", ProfileTypes.SURVIVAL, player.uniqueId) + val playerProfile = profileDataSource.getPlayerData( + ProfileKey.create(ContainerType.WORLD, "world", ProfileTypes.SURVIVAL, player.uniqueId)) ShareHandlingUpdater.updateProfile(multiverseInventories, player, PersistingProfile(Sharables.allOf(), playerProfile)) assertEquals(4.4, playerProfile.get(Sharables.HEALTH)) @@ -37,7 +39,8 @@ class ShareHandlingUpdaterTest : TestWithMockBukkit() { @Test fun `Test updating player`() { - val playerProfile = profileDataSource.getPlayerData(ContainerType.WORLD, "world", ProfileTypes.SURVIVAL, player.uniqueId) + val playerProfile = profileDataSource.getPlayerData( + ProfileKey.create(ContainerType.WORLD, "world", ProfileTypes.SURVIVAL, player.uniqueId)) playerProfile.set(Sharables.HEALTH, 4.4) playerProfile.set(Sharables.MAX_HEALTH, 15.1) diff --git a/src/test/java/org/mvplugins/multiverse/inventories/handleshare/WorldChangeTest.kt b/src/test/java/org/mvplugins/multiverse/inventories/handleshare/WorldChangeTest.kt index 6cb4b5bd..2f14b0d7 100644 --- a/src/test/java/org/mvplugins/multiverse/inventories/handleshare/WorldChangeTest.kt +++ b/src/test/java/org/mvplugins/multiverse/inventories/handleshare/WorldChangeTest.kt @@ -28,13 +28,21 @@ class WorldChangeTest : TestWithMockBukkit() { Logging.fine("player world: " + server.getPlayer("Benji_0224")?.world?.name) val stack = ItemStack.of(Material.STONE_BRICKS, 64) player.inventory.setItem(0, stack) + player.health = 5.5 + val startTime = System.nanoTime() server.getWorld("world2")?.let { player.teleport(it.spawnLocation) } assertEquals(stack, player.inventory.getItem(0)) + assertEquals(5.5, player.health) + server.getWorld("world4")?.let { player.teleport(it.spawnLocation) } assertNotEquals(stack, player.inventory.getItem(0)) + assertNotEquals(5.5, player.health) + server.getWorld("world2")?.let { player.teleport(it.spawnLocation) } assertEquals(stack, player.inventory.getItem(0)) + assertEquals(5.5, player.health) + Logging.info("Time taken: " + (System.nanoTime() - startTime) / 1000000 + "ms") } diff --git a/src/test/java/org/mvplugins/multiverse/inventories/profile/FilePerformanceTest.kt b/src/test/java/org/mvplugins/multiverse/inventories/profile/FilePerformanceTest.kt index 6162324a..0825016c 100644 --- a/src/test/java/org/mvplugins/multiverse/inventories/profile/FilePerformanceTest.kt +++ b/src/test/java/org/mvplugins/multiverse/inventories/profile/FilePerformanceTest.kt @@ -41,7 +41,7 @@ class FilePerformanceTest : TestWithMockBukkit() { fun `Test 10K global profiles`() { val startTime = System.nanoTime() for (i in 0..9999) { - val globalProfile = profileDataSource.getGlobalProfile("player-$i", UUID.randomUUID()) + val globalProfile = profileDataSource.getGlobalProfile(UUID.randomUUID()) globalProfile.setLoadOnLogin(true) profileDataSource.updateGlobalProfile(globalProfile) } @@ -52,7 +52,7 @@ class FilePerformanceTest : TestWithMockBukkit() { val startTime2 = System.nanoTime() for (i in 0..9999) { - val globalProfile = profileDataSource.getGlobalProfile("player-$i", UUID.randomUUID()) + val globalProfile = profileDataSource.getGlobalProfile(UUID.randomUUID()) globalProfile.setLoadOnLogin(false) profileDataSource.updateGlobalProfile(globalProfile) } @@ -67,7 +67,7 @@ class FilePerformanceTest : TestWithMockBukkit() { val player = server.getPlayer(i) for (gameMode in GameMode.entries) { val playerProfile = profileDataSource.getPlayerData( - ContainerType.WORLD, "world", ProfileTypes.forGameMode(gameMode), player.uniqueId) + ProfileKey.create(ContainerType.WORLD, "world", ProfileTypes.forGameMode(gameMode), player.uniqueId)) playerProfile.set(Sharables.HEALTH, 5.0) playerProfile.set(Sharables.OFF_HAND, ItemStack(Material.STONE_BRICKS, 10)) playerProfile.set(Sharables.INVENTORY, arrayOf( @@ -92,8 +92,7 @@ class FilePerformanceTest : TestWithMockBukkit() { val player = server.getPlayer(i) for (gameMode in GameMode.entries) { val playerProfile = profileDataSource.getPlayerData( - ContainerType.WORLD, "world", ProfileTypes.forGameMode(gameMode), player.uniqueId - ) + ProfileKey.create(ContainerType.WORLD, "world", ProfileTypes.forGameMode(gameMode), player.uniqueId)) assertEquals(5.0, playerProfile.get(Sharables.HEALTH)) assertEquals(ItemStack(Material.STONE_BRICKS, 10), playerProfile.get(Sharables.OFF_HAND)) } @@ -105,11 +104,9 @@ class FilePerformanceTest : TestWithMockBukkit() { val player = server.getPlayer(i) for (gameMode in GameMode.entries) { profileDataSource.removePlayerData( - ContainerType.WORLD, "world", ProfileTypes.forGameMode(gameMode), player.uniqueId - ) + ProfileKey.create(ContainerType.WORLD, "world", ProfileTypes.forGameMode(gameMode), player.uniqueId)) val playerProfile = profileDataSource.getPlayerData( - ContainerType.WORLD, "world", ProfileTypes.forGameMode(gameMode), player.uniqueId - ) + ProfileKey.create(ContainerType.WORLD, "world", ProfileTypes.forGameMode(gameMode), player.uniqueId)) assertNull(playerProfile.get(Sharables.HEALTH)) assertNull(playerProfile.get(Sharables.OFF_HAND)) } @@ -119,8 +116,10 @@ class FilePerformanceTest : TestWithMockBukkit() { Thread.sleep(1000) // Wait for files to write finish val cacheStats = profileDataSource.getCacheStats() - Logging.info("Cache stats: $cacheStats") - + Logging.info(cacheStats.values.toString()) + for (cacheStat in cacheStats) { + Logging.info(cacheStat.key + ": " + cacheStat.value.averageLoadPenalty() / 1000000 + "ms") + } } fun createItemStack(material: Material, amount: Int = 1, modify: Consumer): ItemStack {