diff --git a/build.gradle b/build.gradle index 4f73d529..1dc4c9f5 100644 --- a/build.gradle +++ b/build.gradle @@ -39,6 +39,9 @@ dependencies { exclude group: 'junit', module: 'junit' } + // Caching + shadowed("com.github.ben-manes.caffeine:caffeine:3.2.0") + // Other plugins for import compileOnly('uk.co:MultiInv:3.0.6') { exclude group: '*', module: '*' @@ -64,6 +67,8 @@ shadowJar { relocate 'com.dumptruckman.minecraft.util.DebugLog', 'org.mvplugins.multiverse.inventories.utils.DebugFileLogger' relocate 'com.dumptruckman.bukkit.configuration', 'org.mvplugins.multiverse.inventories.utils.configuration' relocate 'net.minidev', 'org.mvplugins.multiverse.inventories.utils.minidev' + relocate 'com.github.benmanes', 'org.mvplugins.multiverse.inventories.utils.benmanes' + relocate 'com.google.errorprone', 'org.mvplugins.multiverse.inventories.utils.errorprone' dependencies { exclude(dependency { 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 83f5a657..8d13683a 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/CacheCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/CacheCommand.java @@ -1,6 +1,6 @@ package org.mvplugins.multiverse.inventories.commands; -import com.google.common.cache.CacheStats; +import com.github.benmanes.caffeine.cache.stats.CacheStats; import org.bukkit.entity.Player; import org.jvnet.hk2.annotations.Service; import org.mvplugins.multiverse.core.commandtools.MVCommandIssuer; @@ -38,7 +38,6 @@ void onCacheStatsCommand(MVCommandIssuer issuer) { 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 + "%"); diff --git a/src/main/java/org/mvplugins/multiverse/inventories/config/InventoriesConfig.java b/src/main/java/org/mvplugins/multiverse/inventories/config/InventoriesConfig.java index 8730bddf..cca41065 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/config/InventoriesConfig.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/config/InventoriesConfig.java @@ -16,6 +16,7 @@ import java.io.IOException; import java.nio.file.Path; +import java.util.List; /** * Provides methods for interacting with the configuration of Multiverse-Inventories. @@ -200,6 +201,21 @@ public Try setAlwaysWriteWorldProfile(boolean alwaysWriteWorldProfile) { return this.configHandle.set(configNodes.alwaysWriteWorldProfile, alwaysWriteWorldProfile); } + public List getPreloadDataOnJoinWorlds() { + return this.configHandle.get(configNodes.preloadDataOnJoinWorlds); + } + + public Try setPreloadDataOnJoinWorlds(List preloadDataOnJoinWorlds) { + return this.configHandle.set(configNodes.preloadDataOnJoinWorlds, preloadDataOnJoinWorlds); + } + + public List getPreloadDataOnJoinGroups() { + return this.configHandle.get(configNodes.preloadDataOnJoinGroups); + } + + public Try setPreloadDataOnJoinGroups(List preloadDataOnJoinGroups) { + return this.configHandle.set(configNodes.preloadDataOnJoinGroups, preloadDataOnJoinGroups); + } public int getPlayerFileCacheSize() { return this.configHandle.get(configNodes.playerFileCacheSize); diff --git a/src/main/java/org/mvplugins/multiverse/inventories/config/InventoriesConfigNodes.java b/src/main/java/org/mvplugins/multiverse/inventories/config/InventoriesConfigNodes.java index 77c867d3..98a9291f 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/config/InventoriesConfigNodes.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/config/InventoriesConfigNodes.java @@ -3,11 +3,13 @@ import org.mvplugins.multiverse.core.configuration.functions.NodeSerializer; import org.mvplugins.multiverse.core.configuration.node.ConfigHeaderNode; import org.mvplugins.multiverse.core.configuration.node.ConfigNode; +import org.mvplugins.multiverse.core.configuration.node.ListConfigNode; import org.mvplugins.multiverse.core.configuration.node.Node; import org.mvplugins.multiverse.core.configuration.node.NodeGroup; import org.mvplugins.multiverse.inventories.share.Sharables; import org.mvplugins.multiverse.inventories.share.Shares; +import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -160,6 +162,20 @@ public Object serialize(Shares sharables, Class aClass) { .name("always-write-world-profile") .build()); + private final ConfigHeaderNode preloadHeader = node(ConfigHeaderNode.builder("performance.preload-data-on-join") + .comment("") + .build()); + + final ListConfigNode preloadDataOnJoinWorlds = node(ListConfigNode.listBuilder("performance.preload-data-on-join.worlds", String.class) + .defaultValue(ArrayList::new) + .name("preload-data-on-join-worlds") + .build()); + + final ListConfigNode preloadDataOnJoinGroups = node(ListConfigNode.listBuilder("performance.preload-data-on-join.groups", String.class) + .defaultValue(ArrayList::new) + .name("preload-data-on-join-groups") + .build()); + private final ConfigHeaderNode cacheHeader = node(ConfigHeaderNode.builder("performance.cache") .comment("") .comment("NOTE: Cache options require a server restart to take effect.") diff --git a/src/main/java/org/mvplugins/multiverse/inventories/dataimport/multiinv/MultiInvImportHelper.java b/src/main/java/org/mvplugins/multiverse/inventories/dataimport/multiinv/MultiInvImportHelper.java index b0639d94..cbc1dd97 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/dataimport/multiinv/MultiInvImportHelper.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/dataimport/multiinv/MultiInvImportHelper.java @@ -106,10 +106,10 @@ private void mergeData(OfflinePlayer player, MIPlayerFileLoader playerFileLoader Logging.warning("Could not import player data for group: " + dataName); return; } - playerProfile = group.getGroupProfileContainer().getPlayerData(ProfileTypes.SURVIVAL, player); + playerProfile = group.getGroupProfileContainer().getPlayerDataNow(ProfileTypes.SURVIVAL, player); } else { playerProfile = profileContainerStoreProvider.getStore(type) - .getContainer(dataName).getPlayerData(ProfileTypes.SURVIVAL, player); + .getContainer(dataName).getPlayerDataNow(ProfileTypes.SURVIVAL, player); } MIInventoryInterface inventoryInterface = playerFileLoader.getInventory(GameMode.SURVIVAL.toString()); 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 30b2c97b..657a4b87 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 @@ -180,7 +180,7 @@ private void saveMVDataForGroup(Group group) throws DataImportException { } private void saveMVDataForPlayer(Group group, OfflinePlayer offlinePlayer) throws DataImportException { - GlobalProfile globalProfile = profileDataSource.getGlobalProfile(offlinePlayer); + GlobalProfile globalProfile = profileDataSource.getGlobalProfileNow(offlinePlayer); globalProfile.setLoadOnLogin(pwiSettings.getProperty(PluginSettings.LOAD_DATA_ON_JOIN)); profileDataSource.updateGlobalProfile(globalProfile); for (GameMode gameMode : GameMode.values()) { @@ -205,10 +205,10 @@ 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(org.mvplugins.multiverse.inventories.profile.ProfileKey + profiles.add(profileDataSource.getPlayerDataNow(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(org.mvplugins.multiverse.inventories.profile.ProfileKey + profiles.add(profileDataSource.getPlayerDataNow(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/dataimport/worldinventories/WorldInventoriesImportHelper.java b/src/main/java/org/mvplugins/multiverse/inventories/dataimport/worldinventories/WorldInventoriesImportHelper.java index 1fdc6ae6..1c74dfc7 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/dataimport/worldinventories/WorldInventoriesImportHelper.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/dataimport/worldinventories/WorldInventoriesImportHelper.java @@ -144,7 +144,7 @@ private Set getWorldsWithoutGroups() { } private void transferData(OfflinePlayer player, Group wiGroup, ProfileContainer profileContainer) { - PlayerProfile playerProfile = profileContainer.getPlayerData(ProfileTypes.SURVIVAL, player); + PlayerProfile playerProfile = profileContainer.getPlayerDataNow(ProfileTypes.SURVIVAL, player); WIPlayerInventory wiInventory = this.loadPlayerInventory(player, wiGroup); WIPlayerStats wiStats = this.loadPlayerStats(player, wiGroup); if (wiInventory != null) { diff --git a/src/main/java/org/mvplugins/multiverse/inventories/event/ShareHandlingEvent.java b/src/main/java/org/mvplugins/multiverse/inventories/event/ShareHandlingEvent.java index 079e866e..414a8857 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/event/ShareHandlingEvent.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/event/ShareHandlingEvent.java @@ -16,13 +16,11 @@ public abstract class ShareHandlingEvent extends Event implements Cancellable { private boolean cancelled; private final Player player; - private final PersistingProfile alwaysWriteProfile; private final List writeProfiles; private final List readProfiles; ShareHandlingEvent(Player player, AffectedProfiles affectedProfiles) { this.player = player; - this.alwaysWriteProfile = affectedProfiles.getAlwaysWriteProfile(); this.writeProfiles = affectedProfiles.getWriteProfiles(); this.readProfiles = affectedProfiles.getReadProfiles(); } @@ -44,17 +42,7 @@ public void setCancelled(boolean cancel) { } /** - * Returns the profile that will always be saved to. By default, this is a profile for the world the player was in. - * - * @return The profile that will always be saved to when this event occurs. - */ - public PersistingProfile getAlwaysWriteProfile() { - return alwaysWriteProfile; - } - - /** - * @return The profiles for the world/groups the player is coming from that data will be saved to in addition to - * the profile returned by {@link #getAlwaysWriteProfile()}. + * @return The profiles for the world/groups the player is coming from that data will be saved to. */ public List getWriteProfiles() { return this.writeProfiles; diff --git a/src/main/java/org/mvplugins/multiverse/inventories/handleshare/AffectedProfiles.java b/src/main/java/org/mvplugins/multiverse/inventories/handleshare/AffectedProfiles.java index 7fffb1f1..dd89aa7b 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/handleshare/AffectedProfiles.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/handleshare/AffectedProfiles.java @@ -5,27 +5,21 @@ import java.util.LinkedList; import java.util.List; - -import static org.mvplugins.multiverse.inventories.share.Sharables.enabled; +import java.util.concurrent.CompletableFuture; public final class AffectedProfiles { - private PersistingProfile alwaysWriteProfile; private final List writeProfiles = new LinkedList<>(); private final List readProfiles = new LinkedList<>(); AffectedProfiles() { } - void setAlwaysWriteProfile(PlayerProfile profile) { - alwaysWriteProfile = new PersistingProfile(enabled(), profile); - } - /** * @param profile The player profile that will need data saved to. * @param shares What from this group needs to be saved. */ - void addWriteProfile(PlayerProfile profile, Shares shares) { + void addWriteProfile(CompletableFuture profile, Shares shares) { writeProfiles.add(new PersistingProfile(shares, profile)); } @@ -33,14 +27,10 @@ void addWriteProfile(PlayerProfile profile, Shares shares) { * @param profile The player profile that will need data loaded from. * @param shares What from this group needs to be loaded. */ - void addReadProfile(PlayerProfile profile, Shares shares) { + void addReadProfile(CompletableFuture profile, Shares shares) { readProfiles.add(new PersistingProfile(shares, profile)); } - public PersistingProfile getAlwaysWriteProfile() { - return alwaysWriteProfile; - } - public List getWriteProfiles() { return writeProfiles; } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/handleshare/GameModeShareHandler.java b/src/main/java/org/mvplugins/multiverse/inventories/handleshare/GameModeShareHandler.java index 568401e7..8e167c93 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/handleshare/GameModeShareHandler.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/handleshare/GameModeShareHandler.java @@ -9,6 +9,7 @@ import org.mvplugins.multiverse.inventories.profile.ProfileTypes; import org.mvplugins.multiverse.inventories.profile.container.ProfileContainer; import org.mvplugins.multiverse.inventories.share.Sharables; +import org.mvplugins.multiverse.inventories.share.Shares; import org.mvplugins.multiverse.inventories.util.Perm; import org.bukkit.GameMode; import org.bukkit.entity.Player; @@ -49,13 +50,19 @@ protected ShareHandlingEvent createEvent() { @Override protected void prepareProfiles() { - Logging.finer("=== " + player.getName() + " changing game mode from: " + fromType + Logging.fine("=== " + player.getName() + " changing game mode from: " + fromType + " to: " + toType + " for world: " + world + " ==="); - affectedProfiles.setAlwaysWriteProfile(worldProfileContainerStore.getContainer(world).getPlayerData(fromType, player)); - if (isPlayerAffectedByChange()) { addProfiles(); + } else if (inventoriesConfig.getAlwaysWriteWorldProfile()) { + // Write to world profile to ensure data is saved incase bypass is removed + affectedProfiles.addWriteProfile( + worldProfileContainerStore.getContainer(world).getPlayerData(player), + (worldGroups.isEmpty() && !inventoriesConfig.getUseOptionalsForUngroupedWorlds()) + ? Sharables.standard() + : Sharables.enabled() + ); } } @@ -73,22 +80,28 @@ private boolean isPlayerBypassingChange() { } private void addProfiles() { - if (hasWorldGroups()) { - worldGroups.forEach(this::addProfilesForWorldGroup); - } else { - Logging.finer("No groups for world."); - affectedProfiles.addReadProfile(worldProfileContainerStore.getContainer(world).getPlayerData(toType, player), - inventoriesConfig.getUseOptionalsForUngroupedWorlds() ? Sharables.enabled() : Sharables.standardOf()); + Shares handledShares = Sharables.noneOf(); + worldGroups.forEach(worldGroup -> addProfilesForWorldGroup(handledShares,worldGroup)); + Shares unhandledShares = Sharables.enabledOf().setSharing(handledShares, false); + if (!unhandledShares.isEmpty()) { + affectedProfiles.addReadProfile(worldProfileContainerStore.getContainer(world).getPlayerData(fromType, player), unhandledShares); } - } - private boolean hasWorldGroups() { - return !worldGroups.isEmpty(); + if (inventoriesConfig.getAlwaysWriteWorldProfile()) { + affectedProfiles.addWriteProfile(worldProfileContainerStore.getContainer(world).getPlayerData(toType, player), + inventoriesConfig.getUseOptionalsForUngroupedWorlds() ? Sharables.enabled() : Sharables.standard()); + } else { + if (!unhandledShares.isEmpty()) { + affectedProfiles.addWriteProfile(worldProfileContainerStore.getContainer(world).getPlayerData(toType, player), unhandledShares); + } + } } - private void addProfilesForWorldGroup(WorldGroup worldGroup) { + private void addProfilesForWorldGroup(Shares handledShares, WorldGroup worldGroup) { ProfileContainer container = worldGroup.getGroupProfileContainer(); - affectedProfiles.addWriteProfile(container.getPlayerData(fromType, player), Sharables.enabled()); - affectedProfiles.addReadProfile(container.getPlayerData(toType, player), Sharables.enabled()); + affectedProfiles.addWriteProfile(container.getPlayerData(fromType, player), worldGroup.getApplicableShares()); + affectedProfiles.addReadProfile(container.getPlayerData(toType, player), worldGroup.getApplicableShares()); + handledShares.addAll(worldGroup.getApplicableShares()); + handledShares.addAll(worldGroup.getDisabledShares()); } } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/handleshare/PersistingProfile.java b/src/main/java/org/mvplugins/multiverse/inventories/handleshare/PersistingProfile.java index ded31a71..575f367d 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/handleshare/PersistingProfile.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/handleshare/PersistingProfile.java @@ -3,11 +3,25 @@ import org.mvplugins.multiverse.inventories.profile.PlayerProfile; import org.mvplugins.multiverse.inventories.share.Shares; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + /** * Simple class for groups that are going to be saved/loaded. This is used specifically for when a user's world * change is being handled. */ -public record PersistingProfile(Shares shares, PlayerProfile profile) { +public final class PersistingProfile { + private final Shares shares; + private final CompletableFuture profile; + + public PersistingProfile(Shares shares, PlayerProfile profile) { + this(shares, CompletableFuture.completedFuture(profile)); + } + + public PersistingProfile(Shares shares, CompletableFuture profile) { + this.shares = shares; + this.profile = profile; + } /** * Gets the shares that will be saved/loaded for the profile. @@ -15,8 +29,7 @@ public record PersistingProfile(Shares shares, PlayerProfile profile) { * @return The shares that will be saved/loaded for the profile. This is the set of all Sharables that will be acted * upon when passed through the ShareHandler class, or any of its subclasses. */ - @Override - public Shares shares() { + public Shares getShares() { return this.shares; } @@ -25,8 +38,7 @@ public Shares shares() { * * @return The player profile for the world/group that will be saved/loaded for. */ - @Override - public PlayerProfile profile() { + public CompletableFuture getProfile() { return this.profile; } } 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 60135610..1da6a7c0 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/handleshare/ShareHandleListener.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/handleshare/ShareHandleListener.java @@ -2,13 +2,13 @@ import com.dumptruckman.minecraft.util.Logging; import org.jvnet.hk2.annotations.Service; -import org.mvplugins.multiverse.core.event.MVConfigReloadEvent; -import org.mvplugins.multiverse.core.event.MVDebugModeEvent; -import org.mvplugins.multiverse.core.event.MVDumpsDebugInfoEvent; import org.mvplugins.multiverse.core.world.WorldManager; +import org.mvplugins.multiverse.external.vavr.control.Try; 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.container.ContainerType; import org.mvplugins.multiverse.inventories.profile.container.ProfileContainerStoreProvider; import org.mvplugins.multiverse.inventories.profile.group.WorldGroup; @@ -41,11 +41,12 @@ import org.mvplugins.multiverse.external.jakarta.inject.Inject; import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; -import java.io.File; import java.io.IOException; +import java.util.ArrayList; import java.util.List; import java.util.UUID; -import java.util.stream.Collectors; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; /** * Events related to handling of player profile changes. @@ -83,6 +84,17 @@ void playerPreLogin(AsyncPlayerPreLoginEvent event) { Logging.finer("Loading global profile for Player{name:'%s', uuid:'%s'}.", event.getName(), event.getUniqueId()); verifyCorrectPlayerName(event.getUniqueId(), event.getName()); + + long startTime = System.nanoTime(); + List> profileFutures = new ArrayList<>(); + config.getPreloadDataOnJoinWorlds().forEach(worldName -> profileFutures.add(profileDataSource.getPlayerData( + ProfileKey.create(ContainerType.WORLD, worldName, ProfileTypes.SURVIVAL, event.getUniqueId(), event.getName())))); + config.getPreloadDataOnJoinGroups().forEach(groupName -> profileFutures.add(profileDataSource.getPlayerData( + ProfileKey.create(ContainerType.GROUP, groupName, ProfileTypes.SURVIVAL, event.getUniqueId(), event.getName())))); + Try.run(() -> CompletableFuture.allOf(profileFutures.toArray(new CompletableFuture[0])).get(10, TimeUnit.SECONDS)) + .onSuccess(ignore -> Logging.finer("Preloaded data for Player{name:'%s', uuid:'%s'}. Time taken: %4.4f ms", + event.getName(), event.getUniqueId(), (System.nanoTime() - startTime) / 1000000.0)) + .onFailure(e -> Logging.warning("Preload data errored out: %s", e.getMessage())); } /** @@ -96,7 +108,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); + final GlobalProfile globalProfile = profileDataSource.getGlobalProfileNow(player); final String world = globalProfile.getLastWorld(); if (config.getApplyPlayerdataOnJoin() && globalProfile.shouldLoadOnLogin()) { ShareHandlingUpdater.updatePlayer(inventories, player, new PersistingProfile( @@ -112,7 +124,7 @@ void playerJoin(final PlayerJoinEvent event) { } private void verifyCorrectPlayerName(UUID uuid, String name) { - profileDataSource.getExistingGlobalProfile(uuid, name).peek(globalProfile -> { + profileDataSource.getExistingGlobalProfileNow(uuid, name).peek(globalProfile -> { if (globalProfile.getLastKnownName().equals(name)) { return; } @@ -141,8 +153,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(); - GlobalProfile globalProfile = profileDataSource.getGlobalProfile(player); - globalProfile.setLastWorld(world); + CompletableFuture globalProfile = profileDataSource.getGlobalProfile(player); + globalProfile.thenAccept(p -> p.setLastWorld(world)); if (config.getSavePlayerdataOnQuit()) { ShareHandlingUpdater.updateProfile(inventories, player, new PersistingProfile( Sharables.allOf(), @@ -151,10 +163,10 @@ void playerQuit(final PlayerQuitEvent event) { .getPlayerData(player) )); if (config.getApplyPlayerdataOnJoin()) { - globalProfile.setLoadOnLogin(true); + globalProfile.thenAccept(p -> p.setLoadOnLogin(true)); } } - profileDataSource.updateGlobalProfile(globalProfile); + globalProfile.thenAccept(profileDataSource::updateGlobalProfile); SingleShareWriter.of(this.inventories, player, Sharables.LAST_LOCATION).write(player.getLocation().clone()); } @@ -208,10 +220,8 @@ void playerChangedWorld(PlayerChangedWorldEvent event) { Logging.fine("The from or to world is not managed by Multiverse-Core!"); } - long startTime = System.nanoTime(); new WorldChangeShareHandler(this.inventories, player, fromWorld.getName(), toWorld.getName()).handleSharing(); profileDataSource.modifyGlobalProfile(player, profile -> profile.setLastWorld(toWorld.getName())); - Logging.finest("WorldChangeShareHandler took " + (System.nanoTime() - startTime) / 1000000 + " ms."); } /** @@ -244,10 +254,10 @@ void playerDeath(PlayerDeathEvent event) { Logging.finer("=== Handling PlayerDeathEvent for: " + event.getEntity().getName() + " ==="); String deathWorld = event.getEntity().getWorld().getName(); ProfileContainer worldProfileContainer = profileContainerStoreProvider.getStore(ContainerType.WORLD).getContainer(deathWorld); - PlayerProfile profile = worldProfileContainer.getPlayerData(event.getEntity()); + PlayerProfile profile = worldProfileContainer.getPlayerDataNow(event.getEntity()); resetStatsOnDeath(event, profile); for (WorldGroup worldGroup : worldGroupManager.getGroupsForWorld(deathWorld)) { - profile = worldGroup.getGroupProfileContainer().getPlayerData(event.getEntity()); + profile = worldGroup.getGroupProfileContainer().getPlayerDataNow(event.getEntity()); resetStatsOnDeath(event, profile); } Logging.finer("=== Finished handling PlayerDeathEvent for: " + event.getEntity().getName() + "! ==="); @@ -276,7 +286,7 @@ void playerRespawn(PlayerRespawnEvent event) { () -> verifyCorrectWorld( player, player.getWorld().getName(), - profileDataSource.getGlobalProfile(player)), + profileDataSource.getGlobalProfileNow(player)), 2L); } @@ -338,11 +348,11 @@ void worldUnload(WorldUnloadEvent event) { ProfileContainer fromWorldProfileContainer = profileContainerStoreProvider.getStore(ContainerType.WORLD) .getContainer(unloadWorldName); - fromWorldProfileContainer.clearContainer(); + fromWorldProfileContainer.clearContainerCache(); List fromGroups = worldGroupManager.getGroupsForWorld(unloadWorldName); for (WorldGroup fromGroup : fromGroups) { - fromGroup.getGroupProfileContainer().clearContainer(); + fromGroup.getGroupProfileContainer().clearContainerCache(); } } } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/handleshare/ShareHandler.java b/src/main/java/org/mvplugins/multiverse/inventories/handleshare/ShareHandler.java index 00d5a735..1504bcb0 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/handleshare/ShareHandler.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/handleshare/ShareHandler.java @@ -1,37 +1,39 @@ package org.mvplugins.multiverse.inventories.handleshare; import com.dumptruckman.minecraft.util.Logging; -import org.mvplugins.multiverse.external.jetbrains.annotations.Nullable; import org.mvplugins.multiverse.inventories.MultiverseInventories; import org.mvplugins.multiverse.inventories.config.InventoriesConfig; import org.mvplugins.multiverse.inventories.event.ShareHandlingEvent; -import org.mvplugins.multiverse.inventories.profile.PlayerProfile; +import org.mvplugins.multiverse.inventories.profile.ProfileDataSnapshot; +import org.mvplugins.multiverse.inventories.profile.ProfileDataSource; import org.mvplugins.multiverse.inventories.profile.container.ContainerType; import org.mvplugins.multiverse.inventories.profile.container.ProfileContainerStore; import org.mvplugins.multiverse.inventories.profile.container.ProfileContainerStoreProvider; import org.mvplugins.multiverse.inventories.profile.group.WorldGroupManager; -import org.mvplugins.multiverse.inventories.share.Shares; +import org.mvplugins.multiverse.inventories.share.Sharables; import org.bukkit.Bukkit; import org.bukkit.entity.Player; -import java.util.List; - /** * Abstract class for handling sharing of data between worlds and game modes. */ sealed abstract class ShareHandler permits WorldChangeShareHandler, GameModeShareHandler { - protected final MultiverseInventories inventories; protected final Player player; + protected final AffectedProfiles affectedProfiles; + + protected final MultiverseInventories inventories; + protected final ProfileDataSource profileDataStore; protected final InventoriesConfig inventoriesConfig; protected final WorldGroupManager worldGroupManager; protected final ProfileContainerStore worldProfileContainerStore; - protected final AffectedProfiles affectedProfiles; ShareHandler(MultiverseInventories inventories, Player player) { - this.inventories = inventories; this.player = player; this.affectedProfiles = new AffectedProfiles(); + + this.inventories = inventories; + this.profileDataStore = inventories.getServiceLocator().getService(ProfileDataSource.class); this.inventoriesConfig = inventories.getServiceLocator().getService(InventoriesConfig.class); this.worldGroupManager = inventories.getServiceLocator().getService(WorldGroupManager.class); this.worldProfileContainerStore = inventories.getServiceLocator() @@ -44,6 +46,7 @@ sealed abstract class ShareHandler permits WorldChangeShareHandler, GameModeShar * inventories/stats for a player and persisting the changes. */ final void handleSharing() { + long startTime = System.nanoTime(); this.prepareProfiles(); ShareHandlingEvent event = this.createEvent(); Bukkit.getPluginManager().callEvent(event); @@ -51,7 +54,12 @@ final void handleSharing() { Logging.fine("Share handling has been cancelled by another plugin!"); return; } - this.completeSharing(event); + logAffectedProfilesCount(); + ProfileDataSnapshot snapshot = getSnapshot(); + updatePlayer(); + updateProfiles(snapshot); + double timeTaken = (System.nanoTime() - startTime) / 1000000.0; + logHandlingComplete(timeTaken, event); } protected abstract void prepareProfiles(); @@ -62,51 +70,51 @@ protected void logBypass() { Logging.fine(player.getName() + " has bypass permission for 1 or more world/groups!"); } - private void completeSharing(ShareHandlingEvent event) { - logAffectedProfilesCount(event); - saveAlwaysWriteProfile(event); - handleProfileChanges(event); - logHandlingComplete(event); - } - - private void logAffectedProfilesCount(ShareHandlingEvent event) { - PersistingProfile alwaysWriteProfile = event.getAlwaysWriteProfile(); - int writeProfiles = event.getWriteProfiles().size() + (alwaysWriteProfile != null ? 1 : 0); + private void logAffectedProfilesCount() { + int writeProfiles = affectedProfiles.getWriteProfiles().size(); Logging.finer("Change affected by %d fromProfiles and %d toProfiles", writeProfiles, - event.getReadProfiles().size()); + affectedProfiles.getReadProfiles().size()); } - private void saveAlwaysWriteProfile(ShareHandlingEvent event) { - if (event.getAlwaysWriteProfile() != null) { - ShareHandlingUpdater.updateProfile(inventories, event.getPlayer(), event.getAlwaysWriteProfile()); - } else { - Logging.warning("No fromWorld to save to"); - } + private ProfileDataSnapshot getSnapshot() { + ProfileDataSnapshot profileDataSnapshot = new ProfileDataSnapshot(); + Sharables.enabled().forEach(sharable -> sharable.getHandler().updateProfile(profileDataSnapshot, player)); + return profileDataSnapshot; } - private void handleProfileChanges(ShareHandlingEvent event) { - if (event.getReadProfiles().isEmpty()) { - Logging.finest("No profiles to read from - nothing more to do."); - } else { - updateProfiles(event.getPlayer(), event.getWriteProfiles()); - updatePlayer(event.getPlayer(), event.getReadProfiles()); + private void updatePlayer() { + for (PersistingProfile readProfile : affectedProfiles.getReadProfiles()) { + ShareHandlingUpdater.updatePlayer(inventories, player, readProfile); } } - private void updateProfiles(Player player, List writeProfiles) { - for (PersistingProfile writeProfile : writeProfiles) { - ShareHandlingUpdater.updateProfile(inventories, player, writeProfile); + private void updateProfiles(ProfileDataSnapshot snapshot) { + if (affectedProfiles.getWriteProfiles().isEmpty()) { + Logging.finest("No profiles to write - nothing more to do."); + return; + } + for (PersistingProfile writeProfile : affectedProfiles.getWriteProfiles()) { + updatePersistingProfile(writeProfile, snapshot); } } - private void updatePlayer(Player player, List readProfiles) { - for (PersistingProfile readProfile : readProfiles) { - ShareHandlingUpdater.updatePlayer(inventories, player, readProfile); + private void updatePersistingProfile(PersistingProfile persistingProfile, ProfileDataSnapshot snapshot) { + if (persistingProfile.getShares().isEmpty()) { + Logging.finest("No shares to write - nothing more to do."); + return; } + persistingProfile.getProfile().thenAcceptAsync(playerProfile -> { + Logging.finer("Persisted: " + persistingProfile.getShares() + " to " + + playerProfile.getContainerType() + ":" + playerProfile.getContainerName() + + " (" + playerProfile.getProfileType() + ")" + + " for player " + playerProfile.getPlayer().getName()); + playerProfile.updateFromSnapshot(snapshot, persistingProfile.getShares()); + profileDataStore.updatePlayerData(playerProfile); + }); } - private void logHandlingComplete(ShareHandlingEvent event) { - Logging.finer("=== %s complete for %s ===", event.getPlayer().getName(), event.getEventName()); + private void logHandlingComplete(double timeTaken, ShareHandlingEvent event) { + Logging.fine("=== %s complete for %s | time taken: %4.4f ms ===", player.getName(), event.getEventName(), timeTaken); } } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/handleshare/ShareHandlingUpdater.java b/src/main/java/org/mvplugins/multiverse/inventories/handleshare/ShareHandlingUpdater.java index 090812c5..e3990e14 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/handleshare/ShareHandlingUpdater.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/handleshare/ShareHandlingUpdater.java @@ -1,15 +1,16 @@ package org.mvplugins.multiverse.inventories.handleshare; import com.dumptruckman.minecraft.util.Logging; +import org.mvplugins.multiverse.external.vavr.control.Try; import org.mvplugins.multiverse.inventories.MultiverseInventories; -import org.mvplugins.multiverse.inventories.config.InventoriesConfig; +import org.mvplugins.multiverse.inventories.profile.PlayerProfile; import org.mvplugins.multiverse.inventories.profile.ProfileDataSource; -import org.mvplugins.multiverse.inventories.profile.container.ContainerType; import org.mvplugins.multiverse.inventories.share.Sharable; import org.bukkit.entity.Player; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; public final class ShareHandlingUpdater { @@ -36,43 +37,50 @@ private ShareHandlingUpdater(MultiverseInventories inventories, Player player, P } private void updateProfile() { - if (profile.shares().isEmpty()) { + if (profile.getShares().isEmpty()) { return; } - for (Sharable sharable : profile.shares()) { - sharable.getHandler().updateProfile(profile.profile(), player); - } - Logging.finer("Persisted: " + profile.shares() + " to " - + profile.profile().getContainerType() + ":" + profile.profile().getContainerName() - + " (" + profile.profile().getProfileType() + ")" - + " for player " + profile.profile().getPlayer().getName()); - inventories.getServiceLocator().getService(ProfileDataSource.class).updatePlayerData(profile.profile()); + Try.of(() -> profile.getProfile().get(10, TimeUnit.SECONDS)) + .peek(playerProfile -> { + for (Sharable sharable : profile.getShares()) { + sharable.getHandler().updateProfile(playerProfile, player); + } + Logging.finer("Persisted: " + profile.getShares() + " to " + + playerProfile.getContainerType() + ":" + playerProfile.getContainerName() + + " (" + playerProfile.getProfileType() + ")" + + " for player " + playerProfile.getPlayer().getName()); + inventories.getServiceLocator().getService(ProfileDataSource.class).updatePlayerData(playerProfile); + }) + .onFailure(e -> Logging.severe("Error getting playerdata: " + e.getMessage())); } private void updatePlayer() { player.closeInventory(); + Try.of(() -> profile.getProfile().get(10, TimeUnit.SECONDS)) + .peek(playerProfile -> { + List> loaded = new ArrayList<>(profile.getShares().size()); + List> defaulted = new ArrayList<>(profile.getShares().size()); - final List> loaded = new ArrayList<>(profile.shares().size()); - final List> defaulted = new ArrayList<>(profile.shares().size()); - - for (Sharable sharable : profile.shares()) { - if (sharable.getHandler().updatePlayer(player, profile.profile())) { - loaded.add(sharable); - } else { - defaulted.add(sharable); - } - } - if (!loaded.isEmpty()) { - Logging.finer("Updated: " + loaded + " for " - + profile.profile().getPlayer().getName() + " for " - + profile.profile().getContainerType() + ":" + profile.profile().getContainerName() - + " (" + profile.profile().getProfileType() + ")"); - } - if (!defaulted.isEmpty()) { - Logging.finer("Defaulted: " + defaulted + " for " - + profile.profile().getPlayer().getName() + " for " - + profile.profile().getContainerType() + ":" + profile.profile().getContainerName() - + " (" + profile.profile().getProfileType() + ")"); - } + for (Sharable sharable : profile.getShares()) { + if (sharable.getHandler().updatePlayer(player, playerProfile)) { + loaded.add(sharable); + } else { + defaulted.add(sharable); + } + } + if (!loaded.isEmpty()) { + Logging.finer("Updated: " + loaded + " for " + + playerProfile.getPlayer().getName() + " for " + + playerProfile.getContainerType() + ":" + playerProfile.getContainerName() + + " (" + playerProfile.getProfileType() + ")"); + } + if (!defaulted.isEmpty()) { + Logging.finer("Defaulted: " + defaulted + " for " + + playerProfile.getPlayer().getName() + " for " + + playerProfile.getContainerType() + ":" + playerProfile.getContainerName() + + " (" + playerProfile.getProfileType() + ")"); + } + }) + .onFailure(e -> Logging.severe("Error getting playerdata: " + e.getMessage()));; } } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/handleshare/SingleShareWriter.java b/src/main/java/org/mvplugins/multiverse/inventories/handleshare/SingleShareWriter.java index 354af032..80a5c555 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/handleshare/SingleShareWriter.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/handleshare/SingleShareWriter.java @@ -49,25 +49,20 @@ public void write(T value, boolean save) { Logging.finer("Writing single share: " + sharable.getNames()[0]); String worldName = this.player.getWorld().getName(); var profileContainerStoreProvider = this.inventories.getServiceLocator().getService(ProfileContainerStoreProvider.class); - writeNewValueToProfile( - profileContainerStoreProvider.getStore(ContainerType.WORLD) - .getContainer(worldName) - .getPlayerData(this.player), - value, - save - ); + profileContainerStoreProvider.getStore(ContainerType.WORLD) + .getContainer(worldName) + .getPlayerData(this.player) + .thenAccept(profile -> writeNewValueToProfile(profile, value, save)); this.inventories.getServiceLocator().getService(WorldGroupManager.class) .getGroupsForWorld(worldName) .forEach(worldGroup -> { - if (worldGroup.getDisabledShares().contains(sharable)) { + if (!worldGroup.getApplicableShares().contains(sharable)) { return; } - writeNewValueToProfile( - worldGroup.getGroupProfileContainer().getPlayerData(this.player), - value, - save - ); + worldGroup.getGroupProfileContainer().getPlayerData(this.player).thenAccept(profile -> { + writeNewValueToProfile(profile, value, save); + }); }); } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/handleshare/SpawnChangeListener.java b/src/main/java/org/mvplugins/multiverse/inventories/handleshare/SpawnChangeListener.java index 65f502d0..fcad9fae 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/handleshare/SpawnChangeListener.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/handleshare/SpawnChangeListener.java @@ -1,9 +1,6 @@ package org.mvplugins.multiverse.inventories.handleshare; -import com.dumptruckman.minecraft.util.Logging; import org.bukkit.Location; -import org.bukkit.block.data.type.Bed; -import org.bukkit.block.data.type.RespawnAnchor; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; @@ -13,8 +10,6 @@ import org.mvplugins.multiverse.inventories.MultiverseInventories; import org.mvplugins.multiverse.inventories.share.Sharables; -import javax.annotation.Nullable; - import static org.mvplugins.multiverse.inventories.util.MinecraftTools.findAnchorFromRespawnLocation; import static org.mvplugins.multiverse.inventories.util.MinecraftTools.findBedFromRespawnLocation; @@ -31,6 +26,9 @@ public SpawnChangeListener(MultiverseInventories inventories) { @EventHandler(priority = EventPriority.MONITOR) void onSpawnChange(PlayerSpawnChangeEvent event) { + if (Sharables.isIgnoringSpawnListener(event.getPlayer())) { + return; + } Player player = event.getPlayer(); if (event.getCause() == Cause.BED) { updatePlayerSpawn(player, findBedFromRespawnLocation(event.getNewSpawn())); diff --git a/src/main/java/org/mvplugins/multiverse/inventories/handleshare/WorldChangeShareHandler.java b/src/main/java/org/mvplugins/multiverse/inventories/handleshare/WorldChangeShareHandler.java index dd9a4d08..9c7399ca 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/handleshare/WorldChangeShareHandler.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/handleshare/WorldChangeShareHandler.java @@ -5,7 +5,6 @@ import org.mvplugins.multiverse.inventories.profile.group.WorldGroup; import org.mvplugins.multiverse.inventories.event.ShareHandlingEvent; import org.mvplugins.multiverse.inventories.event.WorldChangeShareHandlingEvent; -import org.mvplugins.multiverse.inventories.profile.PlayerProfile; import org.mvplugins.multiverse.inventories.profile.container.ProfileContainer; import org.mvplugins.multiverse.inventories.share.Sharables; import org.mvplugins.multiverse.inventories.share.Shares; @@ -42,24 +41,21 @@ protected ShareHandlingEvent createEvent() { @Override protected void prepareProfiles() { - Logging.finer("=== %s traveling from world: %s to world: %s ===", player.getName(), fromWorld, toWorld); - setAlwaysWriteWorldProfile(); + Logging.fine("=== %s traveling from world: %s to world: %s ===", player.getName(), fromWorld, toWorld); if (isPlayerAffectedByChange()) { addWriteProfiles(); addReadProfiles(); + } else if (inventoriesConfig.getAlwaysWriteWorldProfile()) { + // Write to world profile to ensure data is saved incase bypass is removed + affectedProfiles.addWriteProfile( + worldProfileContainerStore.getContainer(fromWorld).getPlayerData(player), + (fromWorldGroups.isEmpty() && !inventoriesConfig.getUseOptionalsForUngroupedWorlds()) + ? Sharables.standard() + : Sharables.enabled() + ); } } - private void setAlwaysWriteWorldProfile() { - // We will always save everything to the world they come from. - PlayerProfile fromWorldProfile = getWorldPlayerProfile(fromWorld, player); - affectedProfiles.setAlwaysWriteProfile(fromWorldProfile); - } - - private PlayerProfile getWorldPlayerProfile(String world, Player player) { - return worldProfileContainerStore.getContainer(world).getPlayerData(player); - } - private boolean isPlayerAffectedByChange() { if (isPlayerBypassingChange()) { logBypass(); @@ -73,11 +69,7 @@ private boolean isPlayerBypassingChange() { } private void addWriteProfiles() { - if (fromWorldGroups.isEmpty()) { - Logging.finer("No groups for fromWorld."); - return; - } - fromWorldGroups.forEach(wg -> new WorldGroupWrapper(wg).conditionallyAddWriteProfiles()); + new WriteProfileAggregator().conditionallyAddWriteProfiles(); } private void addReadProfiles() { @@ -86,11 +78,7 @@ private void addReadProfiles() { private class ReadProfilesAggregator { - private final Shares handledShares; - - private ReadProfilesAggregator() { - this.handledShares = Sharables.noneOf(); - } + private final Shares handledShares = Sharables.noneOf(); private void addReadProfiles() { addReadProfilesFromToWorldGroups(); @@ -138,53 +126,55 @@ private boolean isFromWorldNotInToWorldGroup(WorldGroup worldGroup) { } private void addReadProfileForWorldGroup(WorldGroup worldGroup) { - PlayerProfile playerProfile = getWorldGroupPlayerData(worldGroup); - affectedProfiles.addReadProfile(playerProfile, worldGroup.getApplicableShares()); - } - - private PlayerProfile getWorldGroupPlayerData(WorldGroup worldGroup) { - return worldGroup.getGroupProfileContainer().getPlayerData(player); + affectedProfiles.addReadProfile( + worldGroup.getGroupProfileContainer().getPlayerData(player), + worldGroup.getApplicableShares() + ); } private void useToWorldForMissingShares() { // We need to fill in any sharables that are not going to be transferred with what's saved in the world file. - Shares unhandledShares = Sharables.enabledOf(); + Shares unhandledShares = (toWorldGroups.isEmpty() && !inventoriesConfig.getUseOptionalsForUngroupedWorlds()) + ? Sharables.standardOf() : Sharables.enabledOf(); unhandledShares.removeAll(handledShares); - if (!inventoriesConfig.getUseOptionalsForUngroupedWorlds()) { - unhandledShares.removeAll(Sharables.optional()); - } if (unhandledShares.isEmpty()) { return; } Logging.finer("%s are left unhandled, defaulting to toWorld", unhandledShares); - affectedProfiles.addReadProfile(getToWorldPlayerData(), unhandledShares); - } - - private PlayerProfile getToWorldPlayerData() { - return worldProfileContainerStore.getContainer(toWorld).getPlayerData(player); + affectedProfiles.addReadProfile( + worldProfileContainerStore.getContainer(toWorld).getPlayerData(player), + unhandledShares + ); } } - private class WorldGroupWrapper { - private final WorldGroup worldGroup; + private class WriteProfileAggregator { - public WorldGroupWrapper(WorldGroup worldGroup) { - this.worldGroup = worldGroup; - } + private final Shares handledShares = Sharables.noneOf(); private void conditionallyAddWriteProfiles() { - if (!worldGroup.containsWorld(toWorld)) { - addWriteProfiles(); + fromWorldGroups.forEach(this::conditionallyAddWriteProfileForGroup); + Shares sharesToWrite = inventoriesConfig.getAlwaysWriteWorldProfile() + ? Sharables.enabled() + : Sharables.enabledOf().setSharing(handledShares, false); + if (!sharesToWrite.isEmpty()) { + affectedProfiles.addWriteProfile( + worldProfileContainerStore.getContainer(fromWorld).getPlayerData(player), + sharesToWrite); } } - void addWriteProfiles() { - ProfileContainer container = worldGroup.getGroupProfileContainer(); - affectedProfiles.addWriteProfile(container.getPlayerData(player), getWorldGroupShares()); + private void conditionallyAddWriteProfileForGroup(WorldGroup worldGroup) { + if (!worldGroup.containsWorld(toWorld)) { + addWriteProfileForGroup(worldGroup); + } + handledShares.addAll(worldGroup.getApplicableShares()); + handledShares.addAll(worldGroup.getDisabledShares()); } - private Shares getWorldGroupShares() { - return Sharables.fromShares(worldGroup.getApplicableShares()); + void addWriteProfileForGroup(WorldGroup worldGroup) { + ProfileContainer container = worldGroup.getGroupProfileContainer(); + affectedProfiles.addWriteProfile(container.getPlayerData(player), worldGroup.getApplicableShares()); } } } 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 f6d50c6b..aadca0cd 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/FlatFileProfileDataSource.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/FlatFileProfileDataSource.java @@ -2,9 +2,10 @@ import com.dumptruckman.bukkit.configuration.json.JsonConfiguration; import com.dumptruckman.minecraft.util.Logging; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.CacheStats; +import com.github.benmanes.caffeine.cache.AsyncCache; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.stats.CacheStats; import com.google.common.collect.Sets; import net.minidev.json.parser.JSONParser; import net.minidev.json.parser.ParseException; @@ -27,6 +28,7 @@ import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; @@ -34,8 +36,8 @@ 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.concurrent.TimeoutException; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.logging.Level; @@ -45,42 +47,39 @@ final class FlatFileProfileDataSource implements ProfileDataSource { private static final String JSON = ".json"; - private final JSONParser JSON_PARSER = new JSONParser(JSONParser.USE_INTEGER_STORAGE | JSONParser.ACCEPT_TAILLING_SPACE); + private final ProfileFileIO profileFileIO; - // TODO these probably need configurable max sizes private final Cache playerFileCache; - private final Cache playerProfileCache; - private final Cache globalProfileCache; + private final AsyncCache playerProfileCache; + private final AsyncCache globalProfileCache; private final File worldFolder; private final File groupFolder; private final File playerFolder; - private final ProfileFileIO playerProfileIO; - private final ProfileFileIO globalProfileIO; - @Inject FlatFileProfileDataSource(@NotNull MultiverseInventories plugin, @NotNull InventoriesConfig inventoriesConfig) throws IOException { - this.playerFileCache = CacheBuilder.newBuilder() + this.profileFileIO = new ProfileFileIO(); + + this.playerFileCache = Caffeine.newBuilder() .expireAfterAccess(inventoriesConfig.getPlayerFileCacheExpiry(), TimeUnit.MINUTES) .maximumSize(inventoriesConfig.getPlayerFileCacheSize()) .recordStats() .build(); - this.playerProfileCache = CacheBuilder.newBuilder() + this.playerProfileCache = Caffeine.newBuilder() .expireAfterAccess(inventoriesConfig.getPlayerProfileCacheExpiry(), TimeUnit.MINUTES) .maximumSize(inventoriesConfig.getPlayerProfileCacheSize()) + .executor(profileFileIO.getExecutor()) .recordStats() - .build(); + .buildAsync(); - this.globalProfileCache = CacheBuilder.newBuilder() + this.globalProfileCache = Caffeine.newBuilder() .expireAfterAccess(inventoriesConfig.getGlobalProfileCacheExpiry(), TimeUnit.MINUTES) .maximumSize(inventoriesConfig.getGlobalProfileCacheSize()) + .executor(profileFileIO.getExecutor()) .recordStats() - .build(); - - this.playerProfileIO = new ProfileFileIO(); - this.globalProfileIO = new ProfileFileIO(); + .buildAsync(); // Make the data folders plugin.getDataFolder().mkdirs(); @@ -148,7 +147,8 @@ 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); + Logging.severe("Could not load file %s : %s", file, e.getMessage()); + e.printStackTrace(); throw new RuntimeException(e); }); return jsonConfiguration; @@ -157,7 +157,7 @@ private FileConfiguration parseToConfiguration(File file) { private FileConfiguration getOrLoadProfileFile(ProfileKey profileKey, File playerFile) { ProfileKey fileProfileKey = profileKey.forProfileType(null); return Try.of(() -> - playerFileCache.get(fileProfileKey, () -> playerFile.exists() + playerFileCache.get(fileProfileKey, (key) -> playerFile.exists() ? parseToConfiguration(playerFile) : new JsonConfiguration()) ).getOrElseThrow(e -> { @@ -170,10 +170,10 @@ private FileConfiguration getOrLoadProfileFile(ProfileKey profileKey, File playe * {@inheritDoc} */ @Override - public Future updatePlayerData(PlayerProfile playerProfile) { + public CompletableFuture updatePlayerData(PlayerProfile playerProfile) { ProfileKey profileKey = ProfileKey.fromPlayerProfile(playerProfile); File playerFile = getPlayerFile(profileKey); - return playerProfileIO.queueAction(playerFile, () -> processUpdatePlayerData(profileKey, playerFile, playerProfile.clone())); + return profileFileIO.queueAction(playerFile, () -> processUpdatePlayerData(profileKey, playerFile, playerProfile.clone())); } private void processUpdatePlayerData(ProfileKey profileKey, File playerFile, PlayerProfile playerProfile) { @@ -186,7 +186,7 @@ private void processUpdatePlayerData(ProfileKey profileKey, File playerFile, Pla 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()); + e.printStackTrace(); }); } @@ -227,19 +227,32 @@ private Map serializePlayerProfile(PlayerProfile playerProfile) * {@inheritDoc} */ @Override - public PlayerProfile getPlayerData(ProfileKey key) { + public PlayerProfile getPlayerDataNow(ProfileKey profileKey) { try { - return playerProfileCache.get(key, () -> { + return getPlayerData(profileKey).get(10, TimeUnit.SECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new RuntimeException(e); + } + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture getPlayerData(ProfileKey profileKey) { + try { + return playerProfileCache.get(profileKey, (key, executor) -> { 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 CompletableFuture.completedFuture(PlayerProfile.createPlayerProfile(key.getContainerType(), key.getDataName(), + key.getProfileType(), Bukkit.getOfflinePlayer(key.getPlayerUUID()))); } - return playerProfileIO.waitForData(playerFile, () -> getPlayerDataFromDisk(key, playerFile)); + Logging.finer("%s not cached. loading from disk...", profileKey); + return profileFileIO.queueCallable(playerFile, () -> getPlayerDataFromDisk(key, playerFile)); }); - } catch (ExecutionException e) { - Logging.severe("Could not get data for player: " + key.getPlayerName() - + " for " + key.getContainerType().toString() + ": " + key.getDataName()); + } catch (Exception e) { + Logging.severe("Could not get data for player: " + profileKey.getPlayerName() + + " for " + profileKey.getContainerType().toString() + ": " + profileKey.getDataName()); throw new RuntimeException(e); } } @@ -254,7 +267,7 @@ private PlayerProfile getPlayerDataFromDisk(ProfileKey key, File playerFile) { } catch (IOException e) { Logging.severe("Could not save data for player: " + key.getPlayerName() + " for " + key.getContainerType().toString() + ": " + key.getDataName() + " after conversion."); - Logging.severe(e.getMessage()); + e.printStackTrace(); } } @@ -353,7 +366,7 @@ private void parseJsonPlayerStatsIntoProfile(String stats, PlayerProfile profile } JSONObject jsonStats = null; try { - jsonStats = (JSONObject) JSON_PARSER.parse(stats); + jsonStats = (JSONObject) new JSONParser(JSONParser.USE_INTEGER_STORAGE | JSONParser.ACCEPT_TAILLING_SPACE).parse(stats); } catch (ParseException | ClassCastException e) { Logging.warning("Could not parse stats for player'" + profile.getPlayer().getName() + "' for " + profile.getContainerType() + " '" + profile.getContainerName() + "': " + e.getMessage()); @@ -370,11 +383,11 @@ private void parseJsonPlayerStatsIntoProfile(String stats, PlayerProfile profile * {@inheritDoc} */ @Override - public Future removePlayerData(ProfileKey profileKey) { + public CompletableFuture removePlayerData(ProfileKey profileKey) { File playerFile = getPlayerFile(profileKey); if (profileKey.getProfileType() == null) { for (var type : ProfileTypes.getTypes()) { - Option.of(playerProfileCache.getIfPresent(profileKey.forProfileType(type))) + Option.of(playerProfileCache.synchronous().getIfPresent(profileKey.forProfileType(type))) .peek(profile -> profile.getData().clear()); } if (!playerFile.exists()) { @@ -382,10 +395,10 @@ public Future removePlayerData(ProfileKey profileKey) { + " in " + profileKey.getContainerType() + " " + profileKey.getDataName()); return CompletableFuture.completedFuture(null); } - return playerProfileIO.queueAction(playerFile, playerFile::delete); + return profileFileIO.queueAction(playerFile, playerFile::delete); } - Option.of(playerProfileCache.getIfPresent(profileKey)).peek(profile -> profile.getData().clear()); - return playerProfileIO.queueAction(playerFile, () -> processRemovePlayerData(profileKey, playerFile)); + Option.of(playerProfileCache.synchronous().getIfPresent(profileKey)).peek(profile -> profile.getData().clear()); + return profileFileIO.queueAction(playerFile, () -> processRemovePlayerData(profileKey, playerFile)); } private void processRemovePlayerData(ProfileKey profileKey, File playerFile) { @@ -446,73 +459,102 @@ private void migrateForContainerType(File[] folders, ContainerType containerType @NotNull @Override - public GlobalProfile getGlobalProfile(UUID playerUUID) { - return getGlobalProfile(Bukkit.getOfflinePlayer(playerUUID)); + public GlobalProfile getGlobalProfileNow(UUID playerUUID) { + return getGlobalProfileNow(Bukkit.getOfflinePlayer(playerUUID)); } @NotNull @Override - public GlobalProfile getGlobalProfile(OfflinePlayer player) { - return getGlobalProfile(player.getUniqueId(), player.getName()); + public GlobalProfile getGlobalProfileNow(OfflinePlayer player) { + return getGlobalProfileNow(player.getUniqueId(), player.getName()); } @NotNull @Override - public GlobalProfile getGlobalProfile(UUID playerUUID, String playerName) { + public GlobalProfile getGlobalProfileNow(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); + return getGlobalProfile(playerUUID, playerName).get(10, TimeUnit.SECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { throw new RuntimeException(e); } } @Override - public @NotNull Option getExistingGlobalProfile(UUID playerUUID, String playerName) { - File uuidFile = getGlobalFile(playerUUID.toString()); - if (!uuidFile.exists()) { - return Option.none(); + public @NotNull Option getExistingGlobalProfileNow(UUID playerUUID, String playerName) { + try { + return getExistingGlobalProfile(playerUUID, playerName).get(10, TimeUnit.SECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new RuntimeException(e); } - 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)) { - Logging.warning("Could not properly migrate player global data file for " + playerName); + @Override + public CompletableFuture getGlobalProfile(UUID playerUUID) { + return getGlobalProfile(Bukkit.getOfflinePlayer(playerUUID)); + } + + @Override + public CompletableFuture getGlobalProfile(OfflinePlayer player) { + return getGlobalProfile(player.getUniqueId(), player.getName()); + } + + @NotNull + @Override + public CompletableFuture getGlobalProfile(UUID playerUUID, String playerName) { + try { + File globalFile = getGlobalFile(playerUUID.toString()); + return globalProfileCache.get(playerUUID, (key, executor) -> { + Logging.finer("Global profile for player %s not in cached. Loading...", playerName); + // Migrate from player name to uuid profile file + File legacyFile = getGlobalFile(playerName); + if (legacyFile.exists() && !migrateGlobalProfileToUUID(legacyFile, playerUUID)) { + Logging.warning("Could not properly migrate player global data file for " + playerName); + } + + // Load from existing profile file + if (!globalFile.exists()) { + return CompletableFuture.completedFuture(GlobalProfile.createGlobalProfile(playerUUID, playerName)); + } + return profileFileIO.queueCallable(globalFile, () -> getGlobalProfileFromDisk(playerUUID, playerName, globalFile)); + }); + } catch (Exception e) { + Logging.severe("Unable to get global profile for player: " + playerName); + throw new RuntimeException(e); } + } - // Load from existing profile file + @NotNull + @Override + public CompletableFuture> getExistingGlobalProfile(UUID playerUUID, String playerName) { File uuidFile = getGlobalFile(playerUUID.toString()); if (!uuidFile.exists()) { - return GlobalProfile.createGlobalProfile(playerUUID, playerName); + return CompletableFuture.completedFuture(Option.none()); } - return loadGlobalProfile(uuidFile, playerName, playerUUID); + return getGlobalProfile(playerUUID, playerName).thenApply(Option::of); } private boolean migrateGlobalProfileToUUID(File legacyFile, UUID playerUUID) { return legacyFile.renameTo(getGlobalFile(playerUUID.toString())); } - private GlobalProfile loadGlobalProfile(File globalFile, String playerName, UUID playerUUID) { - FileConfiguration playerData = globalProfileIO.waitForData(globalFile, () -> parseToConfiguration(globalFile)); + private GlobalProfile getGlobalProfileFromDisk(UUID playerUUID, String playerName, File globalFile) { + FileConfiguration playerData = parseToConfiguration(globalFile); ConfigurationSection section = playerData.getConfigurationSection(DataStrings.PLAYER_DATA); if (section == null) { - section = playerData.createSection(DataStrings.PLAYER_DATA); + return GlobalProfile.createGlobalProfile(playerUUID, playerName); } return GlobalProfile.deserialize(playerName, playerUUID, section); } - public Future modifyGlobalProfile(UUID playerUUID, Consumer consumer) { - return modifyGlobalProfile(getGlobalProfile(playerUUID), consumer); + public CompletableFuture modifyGlobalProfile(UUID playerUUID, Consumer consumer) { + return getGlobalProfile(playerUUID).thenCompose(globalProfile -> modifyGlobalProfile(globalProfile, consumer)); } - public Future modifyGlobalProfile(OfflinePlayer offlinePlayer, Consumer consumer) { - return modifyGlobalProfile(getGlobalProfile(offlinePlayer), consumer); + public CompletableFuture modifyGlobalProfile(OfflinePlayer offlinePlayer, Consumer consumer) { + return getGlobalProfile(offlinePlayer).thenCompose(globalProfile -> modifyGlobalProfile(globalProfile, consumer)); } - private Future modifyGlobalProfile(GlobalProfile globalProfile, Consumer consumer) { + private CompletableFuture modifyGlobalProfile(GlobalProfile globalProfile, Consumer consumer) { consumer.accept(globalProfile); return updateGlobalProfile(globalProfile); } @@ -521,9 +563,9 @@ private Future modifyGlobalProfile(GlobalProfile globalProfile, Consumer updateGlobalProfile(GlobalProfile globalProfile) { + public CompletableFuture updateGlobalProfile(GlobalProfile globalProfile) { File globalFile = getGlobalFile(globalProfile.getPlayerUUID().toString()); - return globalProfileIO.queueAction(globalFile, () -> processGlobalProfileWrite(globalProfile, globalFile)); + return profileFileIO.queueAction(globalFile, () -> processGlobalProfileWrite(globalProfile, globalFile)); } private void processGlobalProfileWrite(GlobalProfile globalProfile, File globalFile) { @@ -554,28 +596,28 @@ void clearPlayerCache(UUID playerUUID) { @Override public void clearProfileCache(ProfileKey key) { playerFileCache.invalidate(key); - playerProfileCache.invalidate(key); + playerProfileCache.synchronous().invalidate(key); } @Override public void clearProfileCache(Predicate predicate) { playerFileCache.invalidateAll(Sets.filter(playerFileCache.asMap().keySet(), predicate::test)); - playerProfileCache.invalidateAll(Sets.filter(playerProfileCache.asMap().keySet(), predicate::test)); + playerProfileCache.synchronous().invalidateAll(Sets.filter(playerProfileCache.asMap().keySet(), predicate::test)); } @Override public void clearAllCache() { playerFileCache.invalidateAll(); - globalProfileCache.invalidateAll(); - playerProfileCache.invalidateAll(); + globalProfileCache.synchronous().invalidateAll(); + playerProfileCache.synchronous().invalidateAll(); } @Override public Map getCacheStats() { Map stats = new HashMap<>(); stats.put("playerFileCache", playerFileCache.stats()); - stats.put("globalProfileCache", globalProfileCache.stats()); - stats.put("profileCache", playerProfileCache.stats()); + stats.put("globalProfileCache", globalProfileCache.synchronous().stats()); + stats.put("profileCache", playerProfileCache.synchronous().stats()); return stats; } } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/PlayerProfile.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/PlayerProfile.java index 0f7b1fe9..25f2cac3 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/PlayerProfile.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/PlayerProfile.java @@ -11,21 +11,20 @@ /** * Contains all the world/group specific data for a player. */ -public final class PlayerProfile implements Cloneable { +public final class PlayerProfile extends ProfileDataSnapshot { static PlayerProfile createPlayerProfile(ContainerType containerType, String containerName, ProfileType profileType, OfflinePlayer player) { return new PlayerProfile(containerType, containerName, profileType, player); } - private final Map data = new HashMap<>(Sharables.all().size()); - private final OfflinePlayer player; private final ContainerType containerType; private final String containerName; private final ProfileType profileType; private PlayerProfile(ContainerType containerType, String containerName, ProfileType profileType, OfflinePlayer player) { + super(); this.containerType = containerType; this.profileType = profileType; this.containerName = containerName; @@ -60,38 +59,8 @@ public ProfileType getProfileType() { return this.profileType; } - /** - * Retrieves the profile's value of the {@link Sharable} passed in. - * - * @param sharable Represents the key for the data wanted from the profile. - * @param This indicates the type of return value to be expected. - * @return The value of the sharable for this profile. Null if no value is set. - */ - public T get(Sharable sharable) { - return sharable.getType().cast(this.data.get(sharable)); - } - - /** - * Sets the profile's value for the {@link Sharable} passed in. - * - * @param sharable Represents the key for the data to store. - * @param value The value of the data. - * @param The type of value to be expected. - */ - public void set(Sharable sharable, T value) { - this.data.put(sharable, value); - } - public PlayerProfile clone() { - try { - return (PlayerProfile) super.clone(); - } catch (CloneNotSupportedException e) { - throw new RuntimeException(e); - } - } - - public Map getData() { - return data; + return (PlayerProfile) super.clone(); } @Override diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileData.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileData.java new file mode 100644 index 00000000..6da935a8 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileData.java @@ -0,0 +1,27 @@ +package org.mvplugins.multiverse.inventories.profile; + +import org.mvplugins.multiverse.inventories.share.Sharable; + +import java.util.Map; + +public interface ProfileData { + /** + * Retrieves the profile's value of the {@link Sharable} passed in. + * + * @param sharable Represents the key for the data wanted from the profile. + * @param This indicates the type of return value to be expected. + * @return The value of the sharable for this profile. Null if no value is set. + */ + T get(Sharable sharable); + + /** + * Sets the profile's value for the {@link Sharable} passed in. + * + * @param sharable Represents the key for the data to store. + * @param value The value of the data. + * @param The type of value to be expected. + */ + void set(Sharable sharable, T value); + + Map getData(); +} diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileDataSnapshot.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileDataSnapshot.java new file mode 100644 index 00000000..dbcd74b9 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileDataSnapshot.java @@ -0,0 +1,54 @@ +package org.mvplugins.multiverse.inventories.profile; + +import org.mvplugins.multiverse.inventories.share.Sharable; +import org.mvplugins.multiverse.inventories.share.Sharables; +import org.mvplugins.multiverse.inventories.share.Shares; + +import java.util.HashMap; +import java.util.Map; + +public class ProfileDataSnapshot implements Cloneable, ProfileData { + + private final Map data; + + public ProfileDataSnapshot() { + this.data = new HashMap<>(Sharables.all().size(), 1); + } + + @Override + public T get(Sharable sharable) { + return (T) this.data.get(sharable); + } + + @Override + public void set(Sharable sharable, T value) { + this.data.put(sharable, value); + } + + @Override + public Map getData() { + return data; + } + + public void updateFromSnapshot(ProfileDataSnapshot snapshot) { + this.data.putAll(snapshot.getData()); + } + + public void updateFromSnapshot(ProfileDataSnapshot snapshot, Shares shares) { + shares.forEach(sharable -> { + Object data = snapshot.getData().get(sharable); + if (data != null) { + this.data.put(sharable, data); + } + }); + } + + @Override + public ProfileDataSnapshot clone() { + try { + return (ProfileDataSnapshot) super.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); + } + } +} 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 e9e9d748..e569922f 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileDataSource.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileDataSource.java @@ -1,6 +1,6 @@ package org.mvplugins.multiverse.inventories.profile; -import com.google.common.cache.CacheStats; +import com.github.benmanes.caffeine.cache.stats.CacheStats; import org.bukkit.OfflinePlayer; import org.jetbrains.annotations.NotNull; import org.jvnet.hk2.annotations.Contract; @@ -9,7 +9,7 @@ import java.io.IOException; import java.util.Map; import java.util.UUID; -import java.util.concurrent.Future; +import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import java.util.function.Predicate; @@ -25,7 +25,7 @@ public sealed interface ProfileDataSource permits FlatFileProfileDataSource { * * @param playerProfile The profile for the player that is being updated. */ - Future updatePlayerData(PlayerProfile playerProfile); + CompletableFuture updatePlayerData(PlayerProfile playerProfile); /** * Retrieves a PlayerProfile from the data source. @@ -34,7 +34,16 @@ public sealed interface ProfileDataSource permits FlatFileProfileDataSource { * @return The player as returned from data. If no data was found, a new PlayerProfile will be * created. */ - PlayerProfile getPlayerData(ProfileKey profileKey); + PlayerProfile getPlayerDataNow(ProfileKey profileKey); + + /** + * Retrieves a PlayerProfile from the data source. + * + * @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. + */ + CompletableFuture getPlayerData(ProfileKey profileKey); /** * Removes the persisted data for a player for a specific profile. @@ -42,7 +51,7 @@ public sealed interface ProfileDataSource permits FlatFileProfileDataSource { * @param profileKey The key of the profile to remove. * @return True if successfully removed. */ - Future removePlayerData(ProfileKey profileKey); + CompletableFuture removePlayerData(ProfileKey profileKey); /** * Copies all the data belonging to oldName to newName and removes the old data. @@ -60,7 +69,7 @@ public sealed interface ProfileDataSource permits FlatFileProfileDataSource { * @param playerUUID The UUID of the player. * @return The global profile for the specified player. */ - @NotNull GlobalProfile getGlobalProfile(UUID playerUUID); + @NotNull GlobalProfile getGlobalProfileNow(UUID playerUUID); /** * Retrieves the global profile for a player which contains meta-data for the player. @@ -68,7 +77,7 @@ public sealed interface ProfileDataSource permits FlatFileProfileDataSource { * @param player The player. * @return The global profile for the specified player. */ - @NotNull GlobalProfile getGlobalProfile(OfflinePlayer player); + @NotNull GlobalProfile getGlobalProfileNow(OfflinePlayer player); /** * Retrieves the global profile for a player which contains meta-data for the player. @@ -78,7 +87,7 @@ public sealed interface ProfileDataSource permits FlatFileProfileDataSource { * @param playerName The name of the player. * @return The global profile for the specified player. */ - @NotNull GlobalProfile getGlobalProfile(UUID playerUUID, String playerName); + @NotNull GlobalProfile getGlobalProfileNow(UUID playerUUID, String playerName); /** * Retrieves the global profile for a player which contains meta-data for the player if it exists. @@ -87,18 +96,26 @@ public sealed interface ProfileDataSource permits FlatFileProfileDataSource { * @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(UUID playerUUID, String playerName); + @NotNull Option getExistingGlobalProfileNow(UUID playerUUID, String playerName); + + CompletableFuture getGlobalProfile(UUID playerUUID); + + CompletableFuture getGlobalProfile(OfflinePlayer player); + + @NotNull CompletableFuture getGlobalProfile(UUID playerUUID, String playerName); + + @NotNull CompletableFuture> getExistingGlobalProfile(UUID playerUUID, String playerName); - Future modifyGlobalProfile(UUID playerUUID, Consumer consumer); + CompletableFuture modifyGlobalProfile(UUID playerUUID, Consumer consumer); - Future modifyGlobalProfile(OfflinePlayer offlinePlayer, Consumer consumer); + CompletableFuture modifyGlobalProfile(OfflinePlayer offlinePlayer, Consumer consumer); /** * Update the file for a player's global profile. * * @param globalProfile The GlobalProfile object to update the file for. */ - Future updateGlobalProfile(GlobalProfile globalProfile); + CompletableFuture 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 6082c8c0..82e1cf45 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileFileIO.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileFileIO.java @@ -1,35 +1,36 @@ package org.mvplugins.multiverse.inventories.profile; +import com.dumptruckman.minecraft.util.Logging; + import java.io.File; -import java.util.HashMap; import java.util.Map; -import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.Future; +import java.util.concurrent.ForkJoinPool; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Supplier; final class ProfileFileIO { - private final ExecutorService fileIOExecutorService; + private final ExecutorService fileIOExecutorService = Executors.newWorkStealingPool(); private final Map fileLocks = new ConcurrentHashMap<>(); ProfileFileIO() { - fileIOExecutorService = Executors.newWorkStealingPool(); } - @SuppressWarnings("unchecked") - Future queueAction(File file, Runnable action) { + CompletableFuture queueAction(File file, Runnable action) { CountDownLatch thisLatch = new CountDownLatch(1); CountDownLatch toWaitLatch = fileLocks.put(file, thisLatch); - return (Future) fileIOExecutorService.submit(() -> { - if (toWaitLatch != null) { + CompletableFuture future = new CompletableFuture<>(); + fileIOExecutorService.submit(() -> { + if (toWaitLatch != null && toWaitLatch.getCount() > 0) { try { + Logging.finest("Waiting for lock on " + file); toWaitLatch.await(10, TimeUnit.SECONDS); } catch (InterruptedException e) { throw new RuntimeException(e); @@ -38,15 +39,19 @@ Future queueAction(File file, Runnable action) { action.run(); thisLatch.countDown(); fileLocks.remove(file); + future.complete(null); }); + return future; } - Future queueCallable(File file, Supplier callable) { + CompletableFuture queueCallable(File file, Supplier callable) { CountDownLatch thisLatch = new CountDownLatch(1); CountDownLatch toWaitLatch = fileLocks.put(file, thisLatch); - return fileIOExecutorService.submit(() -> { - if (toWaitLatch != null) { + CompletableFuture future = new CompletableFuture<>(); + fileIOExecutorService.submit(() -> { + if (toWaitLatch != null && toWaitLatch.getCount() > 0) { try { + Logging.finest("Waiting for lock on " + file); toWaitLatch.await(10, TimeUnit.SECONDS); } catch (InterruptedException e) { throw new RuntimeException(e); @@ -55,8 +60,9 @@ Future queueCallable(File file, Supplier callable) { T result = callable.get(); thisLatch.countDown(); fileLocks.remove(file); - return result; + future.complete(result); }); + return future; } T waitForData(File file, Supplier callable) { @@ -66,4 +72,8 @@ T waitForData(File file, Supplier callable) { throw new RuntimeException(e); } } + + ExecutorService getExecutor() { + return fileIOExecutorService; + } } 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 81d8c3b9..3e2c51f1 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileKey.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileKey.java @@ -55,7 +55,7 @@ private ProfileKey(ContainerType containerType, String dataName, ProfileType pro this.profileType = profileType; this.playerUUID = playerUUID; this.playerName = playerName; - this.hashCode = Objects.hashCode(getContainerType(), getDataName(), getProfileType(), getPlayerName(), getPlayerUUID()); + this.hashCode = Objects.hashCode(playerUUID, containerType, dataName, profileType); } public ProfileKey forProfileType(@Nullable ProfileType profileType) { 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 38173eff..fa717833 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 @@ -1,6 +1,5 @@ package org.mvplugins.multiverse.inventories.profile.container; -import com.dumptruckman.minecraft.util.Logging; import org.mvplugins.multiverse.inventories.MultiverseInventories; import org.mvplugins.multiverse.inventories.config.InventoriesConfig; import org.mvplugins.multiverse.inventories.profile.ProfileDataSource; @@ -11,40 +10,40 @@ import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; -import java.util.HashMap; -import java.util.Map; -import java.util.WeakHashMap; +import java.util.concurrent.CompletableFuture; + /** * A container for player profiles in a given world or world group (based on {@link #getContainerType()}), - * using WeakHashMaps to keep memory usage to a minimum. *
* Players may have separate profiles per game mode within this container if game mode profiles are enabled. */ public final class ProfileContainer { - private final Map> playerData = new WeakHashMap<>(); - private final MultiverseInventories inventories; private final String name; private final ContainerType type; private final ProfileDataSource profileDataSource; private final InventoriesConfig config; ProfileContainer(MultiverseInventories inventories, String name, ContainerType type) { - this.inventories = inventories; this.name = name; this.type = type; this.profileDataSource = inventories.getServiceLocator().getService(ProfileDataSource.class); this.config = inventories.getServiceLocator().getService(InventoriesConfig.class); } - /** - * Gets the stored profiles for this player, mapped by ProfileType. - * - * @param name The name of player to get profile map for. - * @return The profile map for the given player. - */ - private Map getPlayerData(String name) { - return this.playerData.computeIfAbsent(name, k -> new HashMap<>()); + public CompletableFuture getPlayerData(Player player) { + ProfileType type = config.getEnableGamemodeShareHandling() + ? ProfileTypes.forGameMode(player.getGameMode()) + : ProfileTypes.SURVIVAL; + return getPlayerData(type, player); + } + + public CompletableFuture getPlayerData(ProfileType profileType, OfflinePlayer player) { + return profileDataSource.getPlayerData(ProfileKey.create( + getContainerType(), + getContainerName(), + profileType, + player)); } /** @@ -55,14 +54,11 @@ private Map getPlayerData(String name) { * @param player Player to get profile for. * @return The profile for the given player. */ - public PlayerProfile getPlayerData(Player player) { - ProfileType type; - if (config.getEnableGamemodeShareHandling()) { - type = ProfileTypes.forGameMode(player.getGameMode()); - } else { - type = ProfileTypes.SURVIVAL; - } - return getPlayerData(type, player); + public PlayerProfile getPlayerDataNow(Player player) { + ProfileType type = config.getEnableGamemodeShareHandling() + ? ProfileTypes.forGameMode(player.getGameMode()) + : ProfileTypes.SURVIVAL; + return getPlayerDataNow(type, player); } /** @@ -72,39 +68,22 @@ public PlayerProfile getPlayerData(Player player) { * @param player Player to get profile for. * @return The profile of the given type for the given player. */ - public PlayerProfile getPlayerData(ProfileType profileType, OfflinePlayer player) { - Map profileMap = this.getPlayerData(player.getName()); - PlayerProfile playerProfile = profileMap.get(profileType); - if (playerProfile == null) { - 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); - } - return playerProfile; - } - - /** - * Adds a player profile to this profile container. - * - * @param playerProfile Player player to add. - */ - public void addPlayerData(PlayerProfile playerProfile) { - this.getPlayerData(playerProfile.getPlayer().getName()).put(playerProfile.getProfileType(), playerProfile); + public PlayerProfile getPlayerDataNow(ProfileType profileType, OfflinePlayer player) { + return profileDataSource.getPlayerDataNow(ProfileKey.create( + getContainerType(), + getContainerName(), + profileType, + player)); } /** * Removes all of the profile data for a given player in this profile container. * * @param player Player to remove data for. + * @return */ - public void removeAllPlayerData(OfflinePlayer player) { - this.getPlayerData(player.getName()).clear(); - profileDataSource.removePlayerData(ProfileKey.create( + public CompletableFuture removeAllPlayerData(OfflinePlayer player) { + return profileDataSource.removePlayerData(ProfileKey.create( getContainerType(), getContainerName(), null, @@ -115,11 +94,11 @@ public void removeAllPlayerData(OfflinePlayer player) { * Removes the profile data for a specific type of profile in this profile container. * * @param profileType The type of profile to remove data for. - * @param player Player to remove data for. + * @param player Player to remove data for. + * @return */ - public void removePlayerData(ProfileType profileType, OfflinePlayer player) { - this.getPlayerData(player.getName()).remove(profileType); - profileDataSource.removePlayerData(ProfileKey.create( + public CompletableFuture removePlayerData(ProfileType profileType, OfflinePlayer player) { + return profileDataSource.removePlayerData(ProfileKey.create( getContainerType(), getContainerName(), profileType, @@ -149,9 +128,8 @@ public ContainerType getContainerType() { /** * Clears all cached data in the container. */ - public void clearContainer() { + public void clearContainerCache() { profileDataSource.clearProfileCache(key -> key.getContainerType().equals(type) && key.getDataName().equals(name)); - this.playerData.clear(); } } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/group/WorldGroup.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/group/WorldGroup.java index f58d7f53..2c2e27de 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/group/WorldGroup.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/group/WorldGroup.java @@ -1,5 +1,6 @@ package org.mvplugins.multiverse.inventories.profile.group; +import com.dumptruckman.minecraft.util.Logging; import org.mvplugins.multiverse.inventories.MultiverseInventories; import org.mvplugins.multiverse.inventories.config.InventoriesConfig; import org.mvplugins.multiverse.inventories.profile.container.ContainerType; @@ -207,6 +208,7 @@ public void recalculateApplicableShares() { Shares disabledOptionalShares = Sharables.optionalOf(); disabledOptionalShares.removeAll(this.inventoriesConfig.getActiveOptionalShares()); this.applicableShares.removeAll(disabledOptionalShares); + Logging.finest("Applicable shares for " + this.getName() + ": " + this.applicableShares); } /** diff --git a/src/main/java/org/mvplugins/multiverse/inventories/share/SharableGroup.java b/src/main/java/org/mvplugins/multiverse/inventories/share/SharableGroup.java index 3f2e9b45..5d9a973d 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/share/SharableGroup.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/share/SharableGroup.java @@ -53,12 +53,12 @@ public Shares compare(Shares shares) { } @Override - public void setSharing(Sharable sharable, boolean sharing) { + public Shares setSharing(Sharable sharable, boolean sharing) { throw new IllegalStateException("May not alter SharableGroup!"); } @Override - public void setSharing(Shares sharables, boolean sharing) { + public Shares setSharing(Shares sharables, boolean sharing) { throw new IllegalStateException("May not alter SharableGroup!"); } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/share/SharableHandler.java b/src/main/java/org/mvplugins/multiverse/inventories/share/SharableHandler.java index 30b3fd16..bf4a342b 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/share/SharableHandler.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/share/SharableHandler.java @@ -2,6 +2,7 @@ import org.mvplugins.multiverse.inventories.profile.PlayerProfile; import org.bukkit.entity.Player; +import org.mvplugins.multiverse.inventories.profile.ProfileData; /** * This class is used to handle the transition of data from a player profile to a player and vice versa, typically @@ -19,7 +20,7 @@ public interface SharableHandler { * with the values of the player. * @param player The player whose values will be used to update the given profile. */ - void updateProfile(PlayerProfile profile, Player player); + void updateProfile(ProfileData profile, Player player); /** * This method is called during share handling (aka PlayerChangeWorldEvent). It will perform updates to @@ -30,5 +31,5 @@ public interface SharableHandler { * @param profile The profile whose values will be used to update the give player. * @return True if player was updated from existing profile. False if default was used (new profile). */ - boolean updatePlayer(Player player, PlayerProfile profile); + boolean updatePlayer(Player player, ProfileData profile); } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/share/Sharables.java b/src/main/java/org/mvplugins/multiverse/inventories/share/Sharables.java index f4e50816..183c9752 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/share/Sharables.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/share/Sharables.java @@ -8,11 +8,11 @@ import org.mvplugins.multiverse.external.vavr.control.Option; import org.mvplugins.multiverse.inventories.MultiverseInventories; import org.mvplugins.multiverse.inventories.config.InventoriesConfig; +import org.mvplugins.multiverse.inventories.profile.ProfileData; import org.mvplugins.multiverse.inventories.profile.group.WorldGroup; import org.mvplugins.multiverse.inventories.profile.group.WorldGroupManager; import org.mvplugins.multiverse.inventories.util.DataStrings; import org.mvplugins.multiverse.inventories.util.PlayerStats; -import org.mvplugins.multiverse.inventories.profile.PlayerProfile; import org.mvplugins.multiverse.inventories.util.MinecraftTools; import org.bukkit.Location; import org.bukkit.Material; @@ -22,6 +22,7 @@ import org.bukkit.potion.PotionEffect; import org.mvplugins.multiverse.core.economy.MVEconomist; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; @@ -31,6 +32,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.UUID; import static org.mvplugins.multiverse.inventories.util.MinecraftTools.findBedFromRespawnLocation; @@ -92,12 +94,12 @@ public static void init(MultiverseInventories inventories) { public static final Sharable ENDER_CHEST = new Sharable.Builder("ender_chest", ItemStack[].class, new SharableHandler() { @Override - public void updateProfile(PlayerProfile profile, Player player) { + public void updateProfile(ProfileData profile, Player player) { profile.set(ENDER_CHEST, player.getEnderChest().getContents()); } @Override - public boolean updatePlayer(Player player, PlayerProfile profile) { + public boolean updatePlayer(Player player, ProfileData profile) { ItemStack[] value = profile.get(ENDER_CHEST); if (value == null) { player.getEnderChest().setContents(MinecraftTools.fillWithAir( @@ -116,21 +118,19 @@ public boolean updatePlayer(Player player, PlayerProfile profile) { public static final Sharable INVENTORY = new Sharable.Builder("inventory_contents", ItemStack[].class, new SharableHandler() { @Override - public void updateProfile(PlayerProfile profile, Player player) { + public void updateProfile(ProfileData profile, Player player) { profile.set(INVENTORY, player.getInventory().getContents()); } @Override - public boolean updatePlayer(Player player, PlayerProfile profile) { + public boolean updatePlayer(Player player, ProfileData profile) { ItemStack[] value = profile.get(INVENTORY); if (value == null) { player.getInventory().setContents(MinecraftTools.fillWithAir( new ItemStack[PlayerStats.INVENTORY_SIZE])); - player.updateInventory(); return false; } player.getInventory().setContents(value); - player.updateInventory(); return true; } }).serializer(new ProfileEntry(false, DataStrings.PLAYER_INVENTORY_CONTENTS), @@ -142,21 +142,19 @@ public boolean updatePlayer(Player player, PlayerProfile profile) { public static final Sharable ARMOR = new Sharable.Builder("armor_contents", ItemStack[].class, new SharableHandler() { @Override - public void updateProfile(PlayerProfile profile, Player player) { + public void updateProfile(ProfileData profile, Player player) { profile.set(ARMOR, player.getInventory().getArmorContents()); } @Override - public boolean updatePlayer(Player player, PlayerProfile profile) { + public boolean updatePlayer(Player player, ProfileData profile) { ItemStack[] value = profile.get(ARMOR); if (value == null) { player.getInventory().setArmorContents(MinecraftTools.fillWithAir( new ItemStack[PlayerStats.ARMOR_SIZE])); - player.updateInventory(); return false; } player.getInventory().setArmorContents(value); - player.updateInventory(); return true; } }).serializer(new ProfileEntry(false, DataStrings.PLAYER_ARMOR_CONTENTS), @@ -168,20 +166,18 @@ public boolean updatePlayer(Player player, PlayerProfile profile) { public static final Sharable OFF_HAND = new Sharable.Builder("off_hand", ItemStack.class, new SharableHandler() { @Override - public void updateProfile(PlayerProfile profile, Player player) { + public void updateProfile(ProfileData profile, Player player) { profile.set(OFF_HAND, player.getInventory().getItemInOffHand()); } @Override - public boolean updatePlayer(Player player, PlayerProfile profile) { + public boolean updatePlayer(Player player, ProfileData profile) { ItemStack value = profile.get(OFF_HAND); if (value == null) { player.getInventory().setItemInOffHand(new ItemStack(Material.AIR)); - player.updateInventory(); return false; } player.getInventory().setItemInOffHand(value); - player.updateInventory(); return true; } }).serializer(new ProfileEntry(false, DataStrings.PLAYER_OFF_HAND_ITEM), @@ -193,12 +189,12 @@ public boolean updatePlayer(Player player, PlayerProfile profile) { public static final Sharable MAX_HEALTH = new Sharable.Builder<>("max_hit_points", Double.class, new SharableHandler() { @Override - public void updateProfile(PlayerProfile profile, Player player) { + public void updateProfile(ProfileData profile, Player player) { profile.set(MAX_HEALTH, getMaxHealth(player)); } @Override - public boolean updatePlayer(Player player, PlayerProfile profile) { + public boolean updatePlayer(Player player, ProfileData profile) { Double value = profile.get(MAX_HEALTH); if (value == null) { Option.of(maxHealthAttr).map(player::getAttribute) @@ -218,7 +214,7 @@ public boolean updatePlayer(Player player, PlayerProfile profile) { public static final Sharable HEALTH = new Sharable.Builder("hit_points", Double.class, new SharableHandler() { @Override - public void updateProfile(PlayerProfile profile, Player player) { + public void updateProfile(ProfileData profile, Player player) { double health = player.getHealth(); // Player is dead, so health should be regained to full. if (health <= 0) { @@ -228,7 +224,7 @@ public void updateProfile(PlayerProfile profile, Player player) { } @Override - public boolean updatePlayer(Player player, PlayerProfile profile) { + public boolean updatePlayer(Player player, ProfileData profile) { Double value = profile.get(HEALTH); if (value == null) { player.setHealth(PlayerStats.HEALTH); @@ -266,12 +262,12 @@ private static double getMaxHealth(Player player) { public static final Sharable REMAINING_AIR = new Sharable.Builder("remaining_air", Integer.class, new SharableHandler() { @Override - public void updateProfile(PlayerProfile profile, Player player) { + public void updateProfile(ProfileData profile, Player player) { profile.set(REMAINING_AIR, player.getRemainingAir()); } @Override - public boolean updatePlayer(Player player, PlayerProfile profile) { + public boolean updatePlayer(Player player, ProfileData profile) { Integer value = profile.get(REMAINING_AIR); if (value == null) { player.setRemainingAir(PlayerStats.REMAINING_AIR); @@ -294,12 +290,12 @@ public boolean updatePlayer(Player player, PlayerProfile profile) { public static final Sharable MAXIMUM_AIR = new Sharable.Builder("maximum_air", Integer.class, new SharableHandler() { @Override - public void updateProfile(PlayerProfile profile, Player player) { + public void updateProfile(ProfileData profile, Player player) { profile.set(MAXIMUM_AIR, player.getMaximumAir()); } @Override - public boolean updatePlayer(Player player, PlayerProfile profile) { + public boolean updatePlayer(Player player, ProfileData profile) { Integer value = profile.get(MAXIMUM_AIR); if (value == null) { player.setMaximumAir(PlayerStats.MAXIMUM_AIR); @@ -322,12 +318,12 @@ public boolean updatePlayer(Player player, PlayerProfile profile) { public static final Sharable FALL_DISTANCE = new Sharable.Builder("fall_distance", Float.class, new SharableHandler() { @Override - public void updateProfile(PlayerProfile profile, Player player) { + public void updateProfile(ProfileData profile, Player player) { profile.set(FALL_DISTANCE, player.getFallDistance()); } @Override - public boolean updatePlayer(Player player, PlayerProfile profile) { + public boolean updatePlayer(Player player, ProfileData profile) { Float value = profile.get(FALL_DISTANCE); if (value == null) { player.setFallDistance(PlayerStats.FALL_DISTANCE); @@ -351,12 +347,12 @@ public boolean updatePlayer(Player player, PlayerProfile profile) { public static final Sharable FIRE_TICKS = new Sharable.Builder("fire_ticks", Integer.class, new SharableHandler() { @Override - public void updateProfile(PlayerProfile profile, Player player) { + public void updateProfile(ProfileData profile, Player player) { profile.set(FIRE_TICKS, player.getFireTicks()); } @Override - public boolean updatePlayer(Player player, PlayerProfile profile) { + public boolean updatePlayer(Player player, ProfileData profile) { Integer value = profile.get(FIRE_TICKS); if (value == null) { player.setFireTicks(PlayerStats.FIRE_TICKS); @@ -381,12 +377,12 @@ public boolean updatePlayer(Player player, PlayerProfile profile) { public static final Sharable EXPERIENCE = new Sharable.Builder("xp", Float.class, new SharableHandler() { @Override - public void updateProfile(PlayerProfile profile, Player player) { + public void updateProfile(ProfileData profile, Player player) { profile.set(EXPERIENCE, player.getExp()); } @Override - public boolean updatePlayer(Player player, PlayerProfile profile) { + public boolean updatePlayer(Player player, ProfileData profile) { Float value = profile.get(EXPERIENCE); if (value == null) { player.setExp(PlayerStats.EXPERIENCE); @@ -409,12 +405,12 @@ public boolean updatePlayer(Player player, PlayerProfile profile) { public static final Sharable LEVEL = new Sharable.Builder("lvl", Integer.class, new SharableHandler() { @Override - public void updateProfile(PlayerProfile profile, Player player) { + public void updateProfile(ProfileData profile, Player player) { profile.set(LEVEL, player.getLevel()); } @Override - public boolean updatePlayer(Player player, PlayerProfile profile) { + public boolean updatePlayer(Player player, ProfileData profile) { Integer value = profile.get(LEVEL); if (value == null) { player.setLevel(PlayerStats.LEVEL); @@ -437,12 +433,12 @@ public boolean updatePlayer(Player player, PlayerProfile profile) { public static final Sharable TOTAL_EXPERIENCE = new Sharable.Builder("total_xp", Integer.class, new SharableHandler() { @Override - public void updateProfile(PlayerProfile profile, Player player) { + public void updateProfile(ProfileData profile, Player player) { profile.set(TOTAL_EXPERIENCE, player.getTotalExperience()); } @Override - public boolean updatePlayer(Player player, PlayerProfile profile) { + public boolean updatePlayer(Player player, ProfileData profile) { Integer value = profile.get(TOTAL_EXPERIENCE); if (value == null) { player.setTotalExperience(PlayerStats.TOTAL_EXPERIENCE); @@ -465,12 +461,12 @@ public boolean updatePlayer(Player player, PlayerProfile profile) { public static final Sharable FOOD_LEVEL = new Sharable.Builder("food_level", Integer.class, new SharableHandler() { @Override - public void updateProfile(PlayerProfile profile, Player player) { + public void updateProfile(ProfileData profile, Player player) { profile.set(FOOD_LEVEL, player.getFoodLevel()); } @Override - public boolean updatePlayer(Player player, PlayerProfile profile) { + public boolean updatePlayer(Player player, ProfileData profile) { Integer value = profile.get(FOOD_LEVEL); if (value == null) { player.setFoodLevel(PlayerStats.FOOD_LEVEL); @@ -494,12 +490,12 @@ public boolean updatePlayer(Player player, PlayerProfile profile) { public static final Sharable EXHAUSTION = new Sharable.Builder("exhaustion", Float.class, new SharableHandler() { @Override - public void updateProfile(PlayerProfile profile, Player player) { + public void updateProfile(ProfileData profile, Player player) { profile.set(EXHAUSTION, player.getExhaustion()); } @Override - public boolean updatePlayer(Player player, PlayerProfile profile) { + public boolean updatePlayer(Player player, ProfileData profile) { Float value = profile.get(EXHAUSTION); if (value == null) { player.setExhaustion(PlayerStats.EXHAUSTION); @@ -523,12 +519,12 @@ public boolean updatePlayer(Player player, PlayerProfile profile) { public static final Sharable SATURATION = new Sharable.Builder("saturation", Float.class, new SharableHandler() { @Override - public void updateProfile(PlayerProfile profile, Player player) { + public void updateProfile(ProfileData profile, Player player) { profile.set(SATURATION, player.getSaturation()); } @Override - public boolean updatePlayer(Player player, PlayerProfile profile) { + public boolean updatePlayer(Player player, ProfileData profile) { Float value = profile.get(SATURATION); if (value == null) { player.setSaturation(PlayerStats.SATURATION); @@ -552,7 +548,7 @@ public boolean updatePlayer(Player player, PlayerProfile profile) { public static final Sharable BED_SPAWN = new Sharable.Builder("bed_spawn", Location.class, new SharableHandler() { @Override - public void updateProfile(PlayerProfile profile, Player player) { + public void updateProfile(ProfileData profile, Player player) { if (inventories.isUsingSpawnChangeEvent()) { // Bed spawn location already updated during PlayerSpawnChangeEvent return; @@ -576,33 +572,44 @@ public void updateProfile(PlayerProfile profile, Player player) { } @Override - public boolean updatePlayer(Player player, PlayerProfile profile) { + public boolean updatePlayer(Player player, ProfileData profile) { Location loc = profile.get(BED_SPAWN); if (loc == null) { Logging.finer("No bed location saved"); + ignoreSpawnListener.add(player.getUniqueId()); player.setBedSpawnLocation(player.getWorld().getSpawnLocation(), true); + ignoreSpawnListener.remove(player.getUniqueId()); return false; } + ignoreSpawnListener.add(player.getUniqueId()); player.setBedSpawnLocation(loc, true); + ignoreSpawnListener.remove(player.getUniqueId()); Logging.finer("updating bed: " + player.getBedSpawnLocation()); return true; } }).serializer(new ProfileEntry(false, DataStrings.PLAYER_BED_SPAWN_LOCATION), new LocationSerializer()) .altName("bedspawn").altName("bed").altName("beds").altName("bedspawns").build(); + // todo: handle this somewhere better + private static List ignoreSpawnListener = new ArrayList<>(); + + public static boolean isIgnoringSpawnListener(Player player) { + return ignoreSpawnListener.contains(player.getUniqueId()); + } + /** * Sharing Last Location. */ public static final Sharable LAST_LOCATION = new Sharable.Builder("last_location", Location.class, new SharableHandler() { @Override - public void updateProfile(PlayerProfile profile, Player player) { + public void updateProfile(ProfileData profile, Player player) { /* It's too late to update the profile for last location here because the world change has already happened. The update occurs in the PlayerTeleportEvent handler in InventoriesListener. */ } @Override - public boolean updatePlayer(Player player, PlayerProfile profile) { + public boolean updatePlayer(Player player, ProfileData profile) { Location loc = profile.get(LAST_LOCATION); if (loc == null || loc.getWorld() == null || loc.equals(player.getLocation())) { return false; @@ -629,7 +636,7 @@ private boolean hasValidEconomyHandler() { } @Override - public void updateProfile(PlayerProfile profile, Player player) { + public void updateProfile(ProfileData profile, Player player) { if (!hasValidEconomyHandler()) { return; } @@ -637,7 +644,7 @@ public void updateProfile(PlayerProfile profile, Player player) { } @Override - public boolean updatePlayer(Player player, PlayerProfile profile) { + public boolean updatePlayer(Player player, ProfileData profile) { if (!hasValidEconomyHandler()) { return false; } @@ -658,13 +665,13 @@ public boolean updatePlayer(Player player, PlayerProfile profile) { public static final Sharable POTIONS = new Sharable.Builder("potion_effects", PotionEffect[].class, new SharableHandler() { @Override - public void updateProfile(PlayerProfile profile, Player player) { + public void updateProfile(ProfileData profile, Player player) { Collection potionEffects = player.getActivePotionEffects(); profile.set(POTIONS, potionEffects.toArray(new PotionEffect[potionEffects.size()])); } @Override - public boolean updatePlayer(Player player, PlayerProfile profile) { + public boolean updatePlayer(Player player, ProfileData profile) { PotionEffect[] effects = profile.get(POTIONS); for (PotionEffect effect : player.getActivePotionEffects()) { player.removePotionEffect(effect.getType()); @@ -947,6 +954,7 @@ public static Shares fromList(List sharesList) { } public static void recalculateEnabledShares() { + Logging.finer("Recalculating enabled shares..."); enabledShares = standardOf(); enabledShares.addAll(inventoriesConfig.getActiveOptionalShares()); worldGroupManager.recalculateApplicableShares(); @@ -1050,19 +1058,20 @@ public void mergeShares(Shares newShares) { * {@inheritDoc} */ @Override - public void setSharing(Sharable sharable, boolean sharing) { + public Shares setSharing(Sharable sharable, boolean sharing) { if (sharing) { this.add(sharable); } else { this.remove(sharable); } + return this; } /** * {@inheritDoc} */ @Override - public void setSharing(Shares sharables, boolean sharing) { + public Shares setSharing(Shares sharables, boolean sharing) { for (Sharable sharable : sharables) { if (sharing) { this.add(sharable); @@ -1070,6 +1079,7 @@ public void setSharing(Shares sharables, boolean sharing) { this.remove(sharable); } } + return this; } @Override diff --git a/src/main/java/org/mvplugins/multiverse/inventories/share/Shares.java b/src/main/java/org/mvplugins/multiverse/inventories/share/Shares.java index 7f289126..631a8eae 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/share/Shares.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/share/Shares.java @@ -40,13 +40,13 @@ public interface Shares extends Cloneable, Iterable, Collection>(10000) for (i in 0..9999) { - val globalProfile = profileDataSource.getGlobalProfile(UUID.randomUUID()) - globalProfile.setLoadOnLogin(true) - futures.add(profileDataSource.updateGlobalProfile(globalProfile)) + futures.add(profileDataSource.modifyGlobalProfile(UUID.randomUUID(), { globalProfile -> + globalProfile.setLoadOnLogin(true) + })) } for (future in futures) { future.get() } Logging.info("Time taken: " + (System.nanoTime() - startTime) / 1000000 + "ms") - - val startTime2 = System.nanoTime() - val futures2 = ArrayList>(10000) - for (i in 0..9999) { - val globalProfile = profileDataSource.getGlobalProfile(UUID.randomUUID()) - globalProfile.setLoadOnLogin(false) - futures2.add(profileDataSource.updateGlobalProfile(globalProfile)) - } - for (future in futures2) { - future.get() - } - Logging.info("Time taken: " + (System.nanoTime() - startTime2) / 1000000 + "ms") } @Test @@ -73,7 +63,7 @@ class FilePerformanceTest : TestWithMockBukkit() { for (i in 0..999) { val player = server.getPlayer(i) for (gameMode in GameMode.entries) { - val playerProfile = profileDataSource.getPlayerData( + val playerProfile = profileDataSource.getPlayerDataNow( 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)) @@ -96,17 +86,22 @@ class FilePerformanceTest : TestWithMockBukkit() { future.get() } Logging.info("Time taken: " + (System.nanoTime() - startTime) / 1000000 + "ms") + profileDataSource.clearAllCache() val startTime2 = System.nanoTime() + val futures2 = ArrayList>(1000) for (i in 0..999) { val player = server.getPlayer(i) for (gameMode in GameMode.entries) { - val playerProfile = profileDataSource.getPlayerData( - 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)) + futures2.add(profileDataSource.getPlayerData( + ProfileKey.create(ContainerType.WORLD, "world", ProfileTypes.forGameMode(gameMode), player.uniqueId))) } } + for (future in futures2) { + val playerProfile = future.get() + assertEquals(5.0, playerProfile.get(Sharables.HEALTH)) + assertEquals(ItemStack(Material.STONE_BRICKS, 10), playerProfile.get(Sharables.OFF_HAND)) + } Logging.info("Time taken: " + (System.nanoTime() - startTime2) / 1000000 + "ms") val startTime3 = System.nanoTime() @@ -116,7 +111,7 @@ class FilePerformanceTest : TestWithMockBukkit() { for (gameMode in GameMode.entries) { futures3.add(profileDataSource.removePlayerData( ProfileKey.create(ContainerType.WORLD, "world", ProfileTypes.forGameMode(gameMode), player.uniqueId))) - val playerProfile = profileDataSource.getPlayerData( + val playerProfile = profileDataSource.getPlayerDataNow( ProfileKey.create(ContainerType.WORLD, "world", ProfileTypes.forGameMode(gameMode), player.uniqueId)) assertNull(playerProfile.get(Sharables.HEALTH)) assertNull(playerProfile.get(Sharables.OFF_HAND)) @@ -146,4 +141,21 @@ class FilePerformanceTest : TestWithMockBukkit() { server.setPlayers(50) Logging.info("Time taken: " + (System.nanoTime() - startTime) / 1000000 + "ms") } + + @Test + fun `Teleport 50 players consecutively`() { + for (i in 0..49) { + writeResourceToConfigFile("/playerdata.json", "worlds/world2/Player$i.json") + } + server.setPlayers(50) + val startTime = System.nanoTime() + for (player in server.playerList.onlinePlayers) { + server.getWorld("world2")?.let { player.teleport(it.spawnLocation) } + } + Logging.info("Time taken: " + (System.nanoTime() - startTime) / 1000000 + "ms") + val cacheStats = profileDataSource.getCacheStats() + for (cacheStat in cacheStats) { + Logging.info(cacheStat.key + ": " + cacheStat.value.averageLoadPenalty() / 1000000 + "ms") + } + } } diff --git a/src/test/java/org/mvplugins/multiverse/inventories/profile/PlayerNameChangeTest.kt b/src/test/java/org/mvplugins/multiverse/inventories/profile/PlayerNameChangeTest.kt index a34ec36a..100b6ff2 100644 --- a/src/test/java/org/mvplugins/multiverse/inventories/profile/PlayerNameChangeTest.kt +++ b/src/test/java/org/mvplugins/multiverse/inventories/profile/PlayerNameChangeTest.kt @@ -1,5 +1,6 @@ package org.mvplugins.multiverse.inventories.profile +import com.dumptruckman.minecraft.util.Logging import org.bukkit.Material import org.bukkit.inventory.ItemStack import org.mockbukkit.mockbukkit.entity.PlayerMock @@ -61,6 +62,8 @@ class PlayerNameChangeTest : TestWithMockBukkit() { assertEquals(5.0, player.health) assertEquals(stack, player.inventory.getItem(0)) + Thread.sleep(100) // wait for files to save + // check files assertTrue(Path.of(multiverseInventories.dataFolder.absolutePath, "worlds", "world", "benthecat10.json").toFile().exists()) assertTrue(Path.of(multiverseInventories.dataFolder.absolutePath, "worlds", "world_nether", "benthecat10.json").toFile().exists()) @@ -75,6 +78,6 @@ class PlayerNameChangeTest : TestWithMockBukkit() { assertFalse(Path.of(multiverseInventories.dataFolder.absolutePath, "groups", "test", "Benji_0224.json").toFile().exists()) // check player profile - assertEquals("benthecat10", profileDataSource.getGlobalProfile(player)?.lastKnownName) + assertEquals("benthecat10", profileDataSource.getGlobalProfileNow(player)?.lastKnownName) } } diff --git a/src/test/java/org/mvplugins/multiverse/inventories/profile/ProfileDataSourceTest.kt b/src/test/java/org/mvplugins/multiverse/inventories/profile/ProfileDataSourceTest.kt index 599011c0..22ba2a19 100644 --- a/src/test/java/org/mvplugins/multiverse/inventories/profile/ProfileDataSourceTest.kt +++ b/src/test/java/org/mvplugins/multiverse/inventories/profile/ProfileDataSourceTest.kt @@ -1,7 +1,29 @@ package org.mvplugins.multiverse.inventories.profile +import com.dumptruckman.minecraft.util.Logging +import org.junit.jupiter.api.Test import org.mvplugins.multiverse.inventories.TestWithMockBukkit +import org.mvplugins.multiverse.inventories.profile.container.ContainerType +import kotlin.test.BeforeTest class ProfileDataSourceTest : TestWithMockBukkit() { - //TODO -} \ No newline at end of file + + private lateinit var profileDataSource: ProfileDataSource + + @BeforeTest + fun setUp() { + profileDataSource = serviceLocator.getService(ProfileDataSource::class.java).takeIf { it != null } ?: run { + throw IllegalStateException("ProfileDataSource is not available as a service") } + } + + @Test + fun `getPlayerData called twice`() { + server.setPlayers(1) + writeResourceToConfigFile("/playerdata.json", "worlds/world/Player0.json") + val key = ProfileKey.create(ContainerType.WORLD, "world", ProfileTypes.SURVIVAL, server.getPlayer("Player0")) + profileDataSource.getPlayerData(key).thenAccept { profile -> Logging.info(profile.toString()) } + profileDataSource.getPlayerData(key).thenAccept { profile -> Logging.info(profile.toString()) } + profileDataSource.getPlayerData(key).thenAccept { profile -> Logging.info(profile.toString()) } + Logging.info("Getting player data...") + } +} diff --git a/src/test/resources/config/fresh_config.yml b/src/test/resources/config/fresh_config.yml index 34efd460..ceec69b5 100644 --- a/src/test/resources/config/fresh_config.yml +++ b/src/test/resources/config/fresh_config.yml @@ -14,6 +14,9 @@ performance: save-playerdata-on-quit: false apply-playerdata-on-join: false always-write-world-profile: true + preload-data-on-join: + worlds: [] + groups: [] cache: player-file-cache-size: 2000 player-file-cache-expiry: 60 diff --git a/src/test/resources/gameplay/gamemode_change_groups.yml b/src/test/resources/gameplay/gamemode_change_groups.yml new file mode 100644 index 00000000..2ff14aa6 --- /dev/null +++ b/src/test/resources/gameplay/gamemode_change_groups.yml @@ -0,0 +1,8 @@ +groups: + group1: + worlds: + - world + shares: + - hit_points + disabled-shares: + - total_xp diff --git a/src/test/resources/playerdata.json b/src/test/resources/playerdata.json new file mode 100644 index 00000000..3de7e0a9 --- /dev/null +++ b/src/test/resources/playerdata.json @@ -0,0 +1 @@ +{"SURVIVAL":{"inventoryContents":{"0":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"REDSTONE_TORCH"},"1":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"REDSTONE_BLOCK"},"2":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"COMPARATOR"},"3":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"LEVER"},"4":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"STONE_BUTTON"},"5":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"REDSTONE"},"8":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"REDSTONE_BLOCK"}},"lastLocation":{"==":"org.bukkit.Location","world":"world","x":-39.5,"y":72.0,"z":0.5,"pitch":0.0,"yaw":0.0},"armorContents":{},"potions":[],"bedSpawnLocation":{"==":"org.bukkit.Location","world":"world","x":-40.0,"y":72.0,"z":0.0,"pitch":0.0,"yaw":0.0},"enderChestContents":{},"offHandItem":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"AIR","amount":0},"stats":{"ex":"0.0","ma":"300","mhp":"20.0","fl":"20","el":"0","hp":"20.0","xp":"0.0","txp":"0","sa":"5.0","ft":"-20","fd":"0.0","ra":"300"}},"CREATIVE":{"inventoryContents":{"22":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"23":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"24":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"25":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"26":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"27":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"28":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"29":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"30":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"31":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"10":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"32":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"11":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"33":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"12":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"DIAMOND","amount":40},"34":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"13":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"35":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"14":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"36":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"DIAMOND_BOOTS"},"15":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"37":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"DIAMOND_LEGGINGS"},"16":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"38":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"DIAMOND_CHESTPLATE"},"17":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"39":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"DIAMOND_HELMET"},"18":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"19":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"0":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"END_STONE","amount":10},"1":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"BONE","amount":2},"2":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"ARROW"},"3":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"CRAFTING_TABLE","amount":63},"4":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"5":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"6":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"7":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"8":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"9":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"40":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"20":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"21":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"}},"lastLocation":{"==":"org.bukkit.Location","world":"world","x":-39.227095053544765,"y":72.0,"z":3.5331800520689565,"pitch":26.250015,"yaw":128.1001},"armorContents":{"0":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"DIAMOND_BOOTS"},"1":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"DIAMOND_LEGGINGS"},"2":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"DIAMOND_CHESTPLATE"},"3":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"DIAMOND_HELMET"}},"potions":[],"bedSpawnLocation":{"==":"org.bukkit.Location","world":"world","x":-40.0,"y":72.0,"z":0.0,"pitch":0.0,"yaw":0.0},"enderChestContents":{},"offHandItem":{"==":"org.bukkit.inventory.ItemStack","v":4189,"type":"SHIELD"},"stats":{"ex":"1.0499994","ma":"300","mhp":"20.0","fl":"17","el":"0","hp":"3.3333358764648438","xp":"0.0","txp":"0","sa":"0.0","ft":"-20","fd":"0.0","ra":"300"}}} \ No newline at end of file