From cc8d10cdee3fd645f23eb7c38e5ef79f3bc5d08c Mon Sep 17 00:00:00 2001 From: sean Date: Mon, 10 Jun 2024 14:05:49 +0200 Subject: [PATCH] Improve performance of friend search & manually cache player profiles The plugin now keeps a sqlite database for every UUID and Username pair. The friend search function now fully runs on a separate thread, and there is now also a "Loading..." screen until the first results are available, especially useful for larger servers. This also switches from the zeroed UUID for representing the "public" to an invalid UUID, since more recent versions of Minecraft don't allow the zeroed UUID anymore. This is therefore a breaking change. --- spigot/build.gradle.kts | 4 + .../de/sean/blockprot/bukkit/BlockProt.java | 32 +++- .../bukkit/CachedProfileService.java | 151 +++++++++++++++ .../bukkit/commands/DebugCommand.java | 8 +- .../bukkit/config/DefaultConfig.java | 2 +- .../inventories/BlockInfoInventory.java | 34 ++-- .../BlockInspectContentsInventory.java | 1 - .../inventories/BlockLockInventory.java | 1 - .../inventories/BlockProtInventory.java | 38 +++- .../inventories/FriendDetailInventory.java | 21 ++- .../inventories/FriendManageInventory.java | 49 +++-- .../FriendSearchHistoryInventory.java | 31 +-- .../inventories/FriendSearchInventory.java | 1 - .../FriendSearchResultInventory.java | 177 ++++++++++++------ .../bukkit/inventories/InventoryState.java | 9 +- .../bukkit/listeners/BlockEventListener.java | 3 - .../bukkit/listeners/HopperEventListener.java | 1 - .../listeners/InventoryEventListener.java | 1 - .../blockprot/bukkit/nbt/FriendHandler.java | 5 +- .../bukkit/nbt/FriendSupportingHandler.java | 27 +-- .../bukkit/nbt/PlayerSettingsHandler.java | 32 +++- 21 files changed, 460 insertions(+), 168 deletions(-) create mode 100644 spigot/src/main/java/de/sean/blockprot/bukkit/CachedProfileService.java diff --git a/spigot/build.gradle.kts b/spigot/build.gradle.kts index 47f6fae8..3e2505ba 100644 --- a/spigot/build.gradle.kts +++ b/spigot/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { // Spigot compileOnly("org.spigotmc:spigot-api:1.20.2-R0.1-SNAPSHOT") + compileOnly("org.apache.commons:commons-lang3:3.13.0") // bStats api("org.bstats:bstats-bukkit:3.0.2") @@ -47,6 +48,7 @@ dependencies { // Dependencies implementation("de.tr7zw:item-nbt-api:$nbtApiVersion") implementation("net.wesjd:anvilgui:$anvilGuiVersion") // Allows us to use anvils as inventories without using NMS. + implementation("org.enginehub:squirrelid:0.3.2") // Integrations implementation("com.github.TownyAdvanced:Towny:$townyVersion") @@ -99,6 +101,7 @@ tasks.shadowJar { relocate("de.tr7zw.changeme.nbtapi", "de.sean.blockprot.bukkit.shaded.nbtapi") relocate("net.wesjd.anvilgui", "de.sean.blockprot.bukkit.shaded.anvilgui") relocate("org.bstats", "de.sean.blockprot.bukkit.metrics") + relocate("org.enginehub.squirrelid", "de.sean.blockprot.bukkit.squirrelid") // minimize() dependencies { @@ -108,6 +111,7 @@ tasks.shadowJar { this.include(dependency("net.wesjd:anvilgui")) this.include(dependency("org.bstats:bstats-base")) this.include(dependency("org.bstats:bstats-bukkit")) + this.include(dependency("org.enginehub:squirrelid")) } archiveClassifier.set( diff --git a/spigot/src/main/java/de/sean/blockprot/bukkit/BlockProt.java b/spigot/src/main/java/de/sean/blockprot/bukkit/BlockProt.java index fe72743b..d66220f1 100644 --- a/spigot/src/main/java/de/sean/blockprot/bukkit/BlockProt.java +++ b/spigot/src/main/java/de/sean/blockprot/bukkit/BlockProt.java @@ -35,13 +35,13 @@ import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; +import org.enginehub.squirrelid.cache.ProfileCache; +import org.enginehub.squirrelid.cache.SQLiteCache; +import org.enginehub.squirrelid.resolver.ProfileService; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.io.BufferedReader; -import java.io.File; -import java.io.InputStream; -import java.io.InputStreamReader; +import java.io.*; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -70,6 +70,11 @@ public final class BlockProt extends JavaPlugin { private final ArrayList integrations = new ArrayList<>(); + @Nullable + private static SQLiteCache playerProfileCache = null; + @Nullable + private static ProfileService playerProfileService = null; + private Metrics metrics; /** @@ -109,10 +114,29 @@ public List getIntegrations() { return Collections.unmodifiableList(integrations); } + @NotNull + public static ProfileCache getProfileCache() { + assert playerProfileCache != null; + return playerProfileCache; + } + + @NotNull + public static ProfileService getProfileService() { + assert playerProfileService != null; + return playerProfileService; + } + @Override public void onLoad() { instance = this; + try { + playerProfileCache = new SQLiteCache(new File(Bukkit.getWorldContainer(), "blockprot_usercache.sqlite")); + playerProfileService = new CachedProfileService(playerProfileCache); + } catch (IOException e) { + throw new RuntimeException("Failed to open SQLite connection to usercache database", e); + } + try { registerIntegration(new WorldGuardIntegration()); } catch (NoClassDefFoundError ignored) {} try { registerIntegration(new TownyIntegration()); } catch (NoClassDefFoundError ignored) {} try { registerIntegration(new PlaceholderAPIIntegration()); } catch (NoClassDefFoundError ignored) {} diff --git a/spigot/src/main/java/de/sean/blockprot/bukkit/CachedProfileService.java b/spigot/src/main/java/de/sean/blockprot/bukkit/CachedProfileService.java new file mode 100644 index 00000000..6819897e --- /dev/null +++ b/spigot/src/main/java/de/sean/blockprot/bukkit/CachedProfileService.java @@ -0,0 +1,151 @@ +package de.sean.blockprot.bukkit; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import org.enginehub.squirrelid.Profile; +import org.enginehub.squirrelid.cache.ProfileCache; +import org.enginehub.squirrelid.resolver.*; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Objects; +import java.util.UUID; +import java.util.function.Predicate; + +public class CachedProfileService implements ProfileService, ProfileCache { + private final ProfileService resolver; + private final ProfileCache cache; + + public CachedProfileService(ProfileCache cache) { + // We try to use our server's cache first for lookups, and only later fallback to the + // Mojang endpoint. + final var services = Lists.newArrayList( + BukkitPlayerService.getInstance(), + PaperPlayerService.getInstance(), + HttpRepositoryService.forMinecraft()); + services.removeIf(Objects::isNull); + this.resolver = new CombinedProfileService(services); + this.cache = cache; + } + + @Override + public int getIdealRequestLimit() { + return this.resolver.getIdealRequestLimit(); + } + + @Nullable + @Override + public Profile findByName(String name) throws IOException, InterruptedException { + final var profile = this.resolver.findByName(name); + if (profile != null) { + put(profile); + } + return profile; + } + + @Override + public ImmutableList findAllByName(Iterable names) throws IOException, InterruptedException { + final var profiles = this.resolver.findAllByName(names); + putAll(profiles); + return profiles; + } + + @Override + public void findAllByName(Iterable names, Predicate predicate) throws IOException, InterruptedException { + this.resolver.findAllByName(names, (input) -> { + put(input); + return predicate.test(input); + }); + } + + @Nullable + @Override + public Profile findByUuid(UUID uuid) { + var profile = getIfPresent(uuid); + if (profile == null) { + try { + profile = this.resolver.findByUuid(uuid); + if (profile != null) { + put(profile); + } + } catch (Exception e) { + return profile; + } + } + return profile; + } + + @Override + public ImmutableList findAllByUuid(Iterable uuids) { + final var map = getAllPresent(uuids); + + if (map.isEmpty()) { + final var profiles = new ArrayList(); + for (final var uuid : uuids) { + try { + final var profile = this.resolver.findByUuid(uuid); + put(profile); + profiles.add(profile); + } catch (Exception e) { + return ImmutableList.copyOf(profiles); + } + } + return ImmutableList.copyOf(profiles); + } + + // The returned map doesn't include the UUIDs which were not found in the cache. + final var lookup = new ArrayList(); + for (final var uuid : uuids) { + if (!map.containsKey(uuid) || map.get(uuid) == null) { + lookup.add(uuid); + } + } + + if (!lookup.isEmpty()) { + final var profiles = new ArrayList<>(map.values()); + for (final var uuid : lookup) { + try { + final var profile = this.resolver.findByUuid(uuid); + put(profile); + profiles.add(profile); + } catch (Exception e) { + return ImmutableList.copyOf(profiles); + } + } + return ImmutableList.copyOf(profiles); + } + + return map.values().asList(); + } + + @Override + public void findAllByUuid(Iterable uuids, Predicate predicate) throws IOException, InterruptedException { + this.resolver.findAllByUuid(uuids, (input) -> { + put(input); + return predicate.test(input); + }); + } + + @Override + public void put(Profile profile) { + this.cache.put(profile); + } + + @Override + public void putAll(Iterable iterable) { + this.cache.putAll(iterable); + } + + @Nullable + @Override + public Profile getIfPresent(UUID uuid) { + return this.cache.getIfPresent(uuid); + } + + @Override + public ImmutableMap getAllPresent(Iterable iterable) { + return this.cache.getAllPresent(iterable); + } +} diff --git a/spigot/src/main/java/de/sean/blockprot/bukkit/commands/DebugCommand.java b/spigot/src/main/java/de/sean/blockprot/bukkit/commands/DebugCommand.java index 8f124c37..c5a21d3b 100644 --- a/spigot/src/main/java/de/sean/blockprot/bukkit/commands/DebugCommand.java +++ b/spigot/src/main/java/de/sean/blockprot/bukkit/commands/DebugCommand.java @@ -19,6 +19,7 @@ package de.sean.blockprot.bukkit.commands; import de.sean.blockprot.bukkit.nbt.BlockNBTHandler; +import de.sean.blockprot.bukkit.nbt.PlayerSettingsHandler; import org.bukkit.Material; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; @@ -49,6 +50,11 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command handler.setOwner("069a79f4-44e9-4726-a5be-fca90e38aaf5"); // Notch's UUID. return true; } + case "clearSearchHistory" -> { + if (!(sender instanceof Player player)) break; + new PlayerSettingsHandler(player).clearSearchHistory(); + return true; + } } return false; } @@ -59,7 +65,7 @@ public List onTabComplete(@NotNull CommandSender sender, @NotNull Comman if (!canUseCommand(sender)) return Collections.emptyList(); - return new ArrayList<>(Arrays.asList("placeDebugChest", "placeDebugShulker")); + return new ArrayList<>(Arrays.asList("placeDebugChest", "placeDebugShulker", "clearSearchHistory")); } @Override diff --git a/spigot/src/main/java/de/sean/blockprot/bukkit/config/DefaultConfig.java b/spigot/src/main/java/de/sean/blockprot/bukkit/config/DefaultConfig.java index 483a5501..195d8bab 100644 --- a/spigot/src/main/java/de/sean/blockprot/bukkit/config/DefaultConfig.java +++ b/spigot/src/main/java/de/sean/blockprot/bukkit/config/DefaultConfig.java @@ -340,7 +340,7 @@ public double getFriendSearchSimilarityPercentage() { * Returns if the friend functionality is fully disabled. This will * no longer allow players to give other players access to their blocks, and * current settings are ignored until re-activated. - * @since 1.1.15 + * @since 1.1.16 */ public boolean isFriendFunctionalityDisabled() { if (!config.contains("disable_friend_functionality")) { diff --git a/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/BlockInfoInventory.java b/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/BlockInfoInventory.java index 13a399e7..5aa5ac19 100644 --- a/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/BlockInfoInventory.java +++ b/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/BlockInfoInventory.java @@ -34,7 +34,6 @@ import org.jetbrains.annotations.NotNull; import java.util.UUID; -import java.util.concurrent.ExecutionException; public class BlockInfoInventory extends BlockProtInventory { private final int maxSkulls = getSize() - InventoryConstants.singleLine; @@ -120,15 +119,14 @@ public Inventory fill(Player player, BlockNBTHandler handler) { var pageOffset = maxSkulls * state.currentPageIndex; for (int i = 0; i < Math.min(friends.size() - pageOffset, maxSkulls); i++) { - var curPlayer = Bukkit.getOfflinePlayer( - UUID.fromString(friends.get(pageOffset + i).getName())); + final var uuid = friends.get(pageOffset + i).getName(); if (friends.get(pageOffset + i).doesRepresentPublic()) { - this.setItemStack(InventoryConstants.lineLength + i, Material.PLAYER_HEAD, TranslationKey.INVENTORIES__FRIENDS__THE_PUBLIC); + this.setItemStack(InventoryConstants.singleLine + i, Material.PLAYER_HEAD, TranslationKey.INVENTORIES__FRIENDS__THE_PUBLIC); } else { - this.setItemStack(InventoryConstants.lineLength + i, Material.SKELETON_SKULL, curPlayer.getName()); + this.setItemStack(InventoryConstants.singleLine + i, Material.SKELETON_SKULL, uuid); } - state.friendResultCache.add(curPlayer); + state.friendResultCache.add(UUID.fromString(uuid)); } if (!owner.isEmpty()) { @@ -175,19 +173,21 @@ public Inventory fill(Player player, BlockNBTHandler handler) { Bukkit.getScheduler().runTaskAsynchronously( BlockProt.getInstance(), () -> { - int i = 0; - while (i < maxSkulls && i < state.friendResultCache.size()) { - if (!state.friendResultCache.get(i).getUniqueId().toString().equals(FriendSupportingHandler.zeroedUuid)) { - var profile = state.friendResultCache.get(i).getPlayerProfile(); - try { - profile = profile.update().get(); - } catch (Exception e) { - BlockProt.getInstance().getLogger().warning("Failed to update PlayerProfile: " + e.getMessage()); - } + try { + final var profiles = BlockProt.getProfileService().findAllByUuid(state.friendResultCache); + + var offset = state.friendResultCache.contains(FriendSupportingHandler.publicUuid) ? 1 : 0; + int i = 0; + while (i < Math.min(maxSkulls, profiles.size())) { + final var profile = profiles.get(i); - setPlayerSkull(InventoryConstants.singleLine + i, profile); + if (!profile.getUniqueId().equals(FriendSupportingHandler.publicUuid)) { + setPlayerSkull(InventoryConstants.singleLine + offset + i, Bukkit.getServer().createPlayerProfile(profile.getUniqueId(), profile.getName())); + } + i++; } - i++; + } catch (Exception e) { + BlockProt.getInstance().getLogger().warning("Failed to update PlayerProfile: " + e.getMessage()); } } ); diff --git a/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/BlockInspectContentsInventory.java b/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/BlockInspectContentsInventory.java index af42dffb..79be4abf 100644 --- a/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/BlockInspectContentsInventory.java +++ b/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/BlockInspectContentsInventory.java @@ -18,7 +18,6 @@ package de.sean.blockprot.bukkit.inventories; -import de.sean.blockprot.bukkit.BlockProt; import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.event.inventory.InventoryClickEvent; diff --git a/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/BlockLockInventory.java b/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/BlockLockInventory.java index f1b01776..2de1dbe0 100644 --- a/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/BlockLockInventory.java +++ b/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/BlockLockInventory.java @@ -25,7 +25,6 @@ import de.sean.blockprot.bukkit.events.BlockAccessMenuEvent; import de.sean.blockprot.bukkit.nbt.BlockNBTHandler; import de.sean.blockprot.bukkit.nbt.PlayerInventoryClipboard; -import de.tr7zw.changeme.nbtapi.utils.MinecraftVersion; import net.wesjd.anvilgui.AnvilGUI; import org.bukkit.Material; import org.bukkit.block.Block; diff --git a/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/BlockProtInventory.java b/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/BlockProtInventory.java index dbfc2676..2476c6fa 100644 --- a/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/BlockProtInventory.java +++ b/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/BlockProtInventory.java @@ -32,7 +32,6 @@ import org.bukkit.Material; import org.bukkit.OfflinePlayer; import org.bukkit.block.Block; -import org.bukkit.block.Skull; import org.bukkit.enchantments.Enchantment; import org.bukkit.entity.HumanEntity; import org.bukkit.entity.Player; @@ -195,6 +194,7 @@ protected final Inventory createInventory(@NotNull String title) { * @param action The action to perform with {@code friend}. * @since 0.4.7 */ + @Deprecated protected final void modifyFriendsForAction( @NotNull final Player player, @NotNull final OfflinePlayer friend, @@ -216,6 +216,36 @@ protected final void modifyFriendsForAction( ); } + /** + * Modifies given {@code friend} for given {@code action}. + * + * @param player The player, or better the owner of the block we want to modify + * or the player we want to edit the default friends for. + * @param friend The friend's UUID we want to do {@code action} for. + * @param action The action to perform with {@code friend}. + * @since 1.1.16 + */ + protected final void modifyFriendsForAction( + @NotNull final Player player, + @NotNull final UUID friend, + @NotNull final FriendModifyAction action + ) { + applyChanges( + player, + (handler) -> handler.modifyFriends( + player.getUniqueId().toString(), + friend.toString(), + action + ), + (handler) -> { + switch (action) { + case ADD_FRIEND -> handler.addFriend(friend.toString()); + case REMOVE_FRIEND -> handler.removeFriend(friend.toString()); + } + } + ); + } + /** * Creates a temporary {@link BlockNBTHandler} or {@link PlayerSettingsHandler} * depending on {@link InventoryState#friendSearchState}, @@ -455,7 +485,7 @@ public void setPlayerSkull(int index, OfflinePlayer player) { * @param profile The player profile whose skull should be used. * @since 1.1.16 */ - public void setPlayerSkull(int index, @NotNull final PlayerProfile profile) { + public void setPlayerSkull(int index, @Nullable final PlayerProfile profile) { final var stack = new ItemStack(Material.PLAYER_HEAD, 1); var meta = (SkullMeta) stack.getItemMeta(); if (meta == null) @@ -464,10 +494,10 @@ public void setPlayerSkull(int index, @NotNull final PlayerProfile profile) { try { assert meta != null; meta.setOwnerProfile(profile); - if (profile.getName() != null) + if (profile != null && profile.getName() != null) meta.setDisplayName(profile.getName()); } catch (Exception e) { - BlockProt.getInstance().getLogger().severe("Failed to set skull head for \"" + profile.getName() + "\": " + e.getMessage()); + BlockProt.getInstance().getLogger().severe("Failed to set skull head for \"" + (profile == null ? "" : profile.getName()) + "\": " + e.getMessage()); } stack.setItemMeta(meta); diff --git a/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/FriendDetailInventory.java b/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/FriendDetailInventory.java index d874f272..1b7f53b2 100644 --- a/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/FriendDetailInventory.java +++ b/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/FriendDetailInventory.java @@ -26,8 +26,8 @@ import de.sean.blockprot.bukkit.nbt.FriendSupportingHandler; import de.sean.blockprot.nbt.FriendModifyAction; import de.tr7zw.changeme.nbtapi.NBTCompound; +import org.bukkit.Bukkit; import org.bukkit.Material; -import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryCloseEvent; @@ -78,8 +78,9 @@ public void onClick(@NotNull InventoryClickEvent event, @NotNull InventoryState switch (item.getType()) { case BLACK_STAINED_GLASS_PANE -> closeAndOpen(player, new FriendManageInventory().fill(player)); case RED_STAINED_GLASS_PANE -> { - OfflinePlayer friend = state.currentFriend; + final var friend = state.currentFriend; assert friend != null; + modifyFriendsForAction(player, friend, FriendModifyAction.REMOVE_FRIEND); // We remove the friend, so the player does not exist anymore either. this.playerHandler = null; @@ -123,11 +124,17 @@ public Inventory fill(@NotNull Player player) { final InventoryState state = InventoryState.get(player.getUniqueId()); if (state == null) return inventory; - final OfflinePlayer friend = state.currentFriend; - if (friend == null) return inventory; + final var uuid = state.currentFriend; + if (uuid == null) return inventory; - if (!state.currentFriend.getUniqueId().toString().equals(FriendSupportingHandler.zeroedUuid)) { - setPlayerSkull(0, state.currentFriend.getPlayerProfile()); + if (!uuid.equals(FriendSupportingHandler.publicUuid)) { + try { + final var profile = BlockProt.getProfileService().findByUuid(uuid); + assert profile != null; + setPlayerSkull(0, Bukkit.getServer().createPlayerProfile(profile.getUniqueId(), profile.getName())); + } catch (Exception e) { + BlockProt.getInstance().getLogger().warning("Failed to find PlayerProfile: " + uuid); + } } else { setItemStack(0, Material.PLAYER_HEAD, TranslationKey.INVENTORIES__FRIENDS__THE_PUBLIC, @@ -141,7 +148,7 @@ public Inventory fill(@NotNull Player player) { if (handler == null) return null; final Optional friendHandler = - handler.getFriend(friend.getUniqueId().toString()); + handler.getFriend(uuid.toString()); if (friendHandler.isEmpty()) { BlockProt.getInstance().getLogger().warning( diff --git a/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/FriendManageInventory.java b/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/FriendManageInventory.java index ec108097..aff1f954 100644 --- a/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/FriendManageInventory.java +++ b/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/FriendManageInventory.java @@ -24,7 +24,6 @@ import de.sean.blockprot.bukkit.inventories.InventoryState.FriendSearchState; import de.sean.blockprot.bukkit.nbt.BlockNBTHandler; import de.sean.blockprot.bukkit.nbt.FriendSupportingHandler; -import de.sean.blockprot.bukkit.nbt.PlayerSettingsHandler; import de.sean.blockprot.nbt.LockReturnValue; import de.tr7zw.changeme.nbtapi.NBTCompound; import org.bukkit.Bukkit; @@ -34,11 +33,11 @@ import org.bukkit.event.inventory.InventoryCloseEvent; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.SkullMeta; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.List; -import java.util.Objects; import java.util.UUID; public final class FriendManageInventory extends BlockProtInventory { @@ -116,9 +115,9 @@ public void onClick(@NotNull InventoryClickEvent event, @NotNull InventoryState } case SKELETON_SKULL, PLAYER_HEAD -> { // Get the clicked player head and open the detail inventory. - var index = findItemIndex(item); - if (index >= 0 && index < state.friendResultCache.size()) { - state.currentFriend = state.friendResultCache.get(index); + final var meta = (SkullMeta) item.getItemMeta(); + if (meta != null) { + state.currentFriend = meta.getOwningPlayer().getUniqueId(); var inv = new FriendDetailInventory().fill(player); closeAndOpen(player, inv); } @@ -162,23 +161,20 @@ public Inventory fill(@NotNull Player player) { state.friendResultCache.clear(); this.inventory.clear(); - var has_added_public = false; + var hasAddedPublic = false; var pageOffset = maxSkulls * state.currentPageIndex; for (int i = 0; i < Math.min(friends.size() - pageOffset, maxSkulls); i++) { - // As far as I can tell Bukkit.getOfflinePlayer will return even if the player - // cannot be found. This allows us to use the zeroed UUID for representing the public. - var curPlayer = Bukkit.getOfflinePlayer( - UUID.fromString(friends.get(pageOffset + i).getName())); + final var uuid = friends.get(pageOffset + i).getName(); if (friends.get(pageOffset + i).doesRepresentPublic()) { this.setItemStack(i, Material.PLAYER_HEAD, TranslationKey.INVENTORIES__FRIENDS__THE_PUBLIC, List.of(Translator.get(TranslationKey.INVENTORIES__FRIENDS__THE_PUBLIC_DESC))); - has_added_public = true; + hasAddedPublic = true; } else { - this.setItemStack(i, Material.SKELETON_SKULL, curPlayer.getName()); + this.setItemStack(i, Material.SKELETON_SKULL, uuid); } - state.friendResultCache.add(curPlayer); + state.friendResultCache.add(UUID.fromString(uuid)); } setItemStack( @@ -190,7 +186,7 @@ public Inventory fill(@NotNull Player player) { Material.BLUE_STAINED_GLASS_PANE, TranslationKey.INVENTORIES__NEXT_PAGE); - if (!has_added_public) { + if (!hasAddedPublic) { setItemStack( getSize() - 4, Material.WITHER_SKELETON_SKULL, @@ -209,23 +205,24 @@ public Inventory fill(@NotNull Player player) { Bukkit.getScheduler().runTaskAsynchronously( BlockProt.getInstance(), () -> { - int i = 0; - while (i < maxSkulls && i < state.friendResultCache.size()) { - if (!state.friendResultCache.get(i).getUniqueId().toString().equals(FriendSupportingHandler.zeroedUuid)) { - var profile = state.friendResultCache.get(i).getPlayerProfile(); - try { - profile = profile.update().get(); - } catch (Exception e) { - BlockProt.getInstance().getLogger().warning("Failed to update PlayerProfile: " + e.getMessage()); - } + try { + final var profiles = BlockProt.getProfileService().findAllByUuid(state.friendResultCache); + + var offset = state.friendResultCache.contains(FriendSupportingHandler.publicUuid) ? 1 : 0; + int i = 0; + while (i < Math.min(maxSkulls, profiles.size())) { + final var profile = profiles.get(i); - setPlayerSkull(InventoryConstants.singleLine + i, profile); + if (!profile.getUniqueId().equals(FriendSupportingHandler.publicUuid)) { + setPlayerSkull(offset + i, Bukkit.getServer().createPlayerProfile(profile.getUniqueId(), profile.getName())); + } + i++; } - i++; + } catch (Exception e) { + BlockProt.getInstance().getLogger().warning("Failed to update PlayerProfile: " + e.getMessage()); } }); return inventory; } } - diff --git a/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/FriendSearchHistoryInventory.java b/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/FriendSearchHistoryInventory.java index 82d39619..d5d231da 100644 --- a/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/FriendSearchHistoryInventory.java +++ b/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/FriendSearchHistoryInventory.java @@ -25,12 +25,12 @@ import de.sean.blockprot.nbt.FriendModifyAction; import org.bukkit.Bukkit; import org.bukkit.Material; -import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryCloseEvent; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.SkullMeta; import org.jetbrains.annotations.NotNull; import java.util.List; @@ -62,10 +62,10 @@ public void onClick(@NotNull InventoryClickEvent event, @NotNull InventoryState switch (item.getType()) { case BLACK_STAINED_GLASS_PANE -> closeAndOpen(player, new FriendManageInventory().fill(player)); case PLAYER_HEAD, SKELETON_SKULL -> { - int index = findItemIndex(item); - if (index >= 0 && index < state.friendResultCache.size()) { - OfflinePlayer friend = state.friendResultCache.get(index); - modifyFriendsForAction(player, friend, FriendModifyAction.ADD_FRIEND); + final var meta = (SkullMeta) item.getItemMeta(); + if (meta != null) { + final var id = meta.getOwningPlayer().getUniqueId(); + modifyFriendsForAction(player, id, FriendModifyAction.ADD_FRIEND); closeAndOpen(player, new FriendManageInventory().fill(player)); } } @@ -88,7 +88,7 @@ public Inventory fill(Player player) { final int max = Math.min(searchHistory.size(), maxSkulls); for (int i = 0; i < max; i++) { this.setItemStack(i, Material.SKELETON_SKULL, searchHistory.get(i)); - state.friendResultCache.add(Bukkit.getOfflinePlayer(UUID.fromString(searchHistory.get(i)))); + state.friendResultCache.add(UUID.fromString(searchHistory.get(i))); } setBackButton(); @@ -96,15 +96,18 @@ public Inventory fill(Player player) { Bukkit.getScheduler().runTaskAsynchronously( BlockProt.getInstance(), () -> { - for (int i = 0; i < max; i++) { - var profile = state.friendResultCache.get(i).getPlayerProfile(); - try { - profile = profile.update().get(); - } catch (Exception e) { - BlockProt.getInstance().getLogger().warning("Failed to update PlayerProfile: " + e.getMessage()); - } + try { + final var profiles = BlockProt.getProfileService().findAllByUuid(state.friendResultCache); + + int i = 0; + while (i < Math.min(profiles.size(), maxSkulls)) { + final var profile = profiles.get(i); - setPlayerSkull(i, profile); + setPlayerSkull(i, Bukkit.getServer().createPlayerProfile(profile.getUniqueId(), profile.getName())); + i++; + } + } catch (Exception e) { + BlockProt.getInstance().getLogger().warning("Failed to update PlayerProfile: " + e.getMessage()); } } ); diff --git a/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/FriendSearchInventory.java b/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/FriendSearchInventory.java index 516288a4..e1b70937 100644 --- a/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/FriendSearchInventory.java +++ b/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/FriendSearchInventory.java @@ -27,7 +27,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; -import java.util.Arrays; import java.util.Collections; import java.util.List; diff --git a/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/FriendSearchResultInventory.java b/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/FriendSearchResultInventory.java index 91ad3578..0c25b66e 100644 --- a/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/FriendSearchResultInventory.java +++ b/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/FriendSearchResultInventory.java @@ -24,6 +24,7 @@ import de.sean.blockprot.bukkit.integrations.PluginIntegration; import de.sean.blockprot.bukkit.nbt.PlayerSettingsHandler; import de.sean.blockprot.nbt.FriendModifyAction; +import org.apache.commons.lang3.tuple.ImmutablePair; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.OfflinePlayer; @@ -32,14 +33,24 @@ import org.bukkit.event.inventory.InventoryCloseEvent; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.SkullMeta; +import org.bukkit.scheduler.BukkitTask; +import org.enginehub.squirrelid.Profile; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.stream.StreamSupport; - -import static java.util.Arrays.spliterator; +import java.util.Arrays; +import java.util.Objects; +import java.util.concurrent.ConcurrentLinkedQueue; public class FriendSearchResultInventory extends BlockProtInventory { + final ConcurrentLinkedQueue resultQueue = new ConcurrentLinkedQueue<>(); + + private final int maxResults = getSize() - 1; + + BukkitTask loadTask = null; + BukkitTask updateTask = null; + @Override int getSize() { return InventoryConstants.tripleLine; @@ -104,15 +115,15 @@ public void onClick(@NotNull InventoryClickEvent event, @NotNull InventoryState new FriendManageInventory().fill(player) ); case PLAYER_HEAD, SKELETON_SKULL -> { - int index = findItemIndex(item); - if (index >= 0 && index < state.friendResultCache.size()) { - OfflinePlayer friend = state.friendResultCache.get(index); - modifyFriendsForAction(player, friend, FriendModifyAction.ADD_FRIEND); + final var meta = (SkullMeta) item.getItemMeta(); + if (meta != null) { + final var id = meta.getOwningPlayer().getUniqueId(); + modifyFriendsForAction(player, id, FriendModifyAction.ADD_FRIEND); closeAndOpen(player, new FriendManageInventory().fill(player)); // Update the search history PlayerSettingsHandler settingsHandler = new PlayerSettingsHandler(player); - settingsHandler.addPlayerToSearchHistory(friend); + settingsHandler.addPlayerToSearchHistory(id); } } default -> closeAndOpen(player, null); @@ -122,7 +133,10 @@ public void onClick(@NotNull InventoryClickEvent event, @NotNull InventoryState @Override public void onClose(@NotNull InventoryCloseEvent event, @NotNull InventoryState state) { - + if (loadTask != null) + loadTask.cancel(); + if (updateTask != null) + updateTask.cancel(); } /** @@ -146,64 +160,103 @@ public Inventory fill(@NotNull Player player, String searchQuery) { InventoryState state = InventoryState.get(player.getUniqueId()); if (state == null) return inventory; - // The already existing friends we want to add to. - final @Nullable var handler = - getFriendSupportingHandler(state.friendSearchState, player, state.getBlock()); - if (handler == null) return null; // return null to indicate an issue. - - // We'll filter all doubled friends out of the list and add them to the current InventoryState. - double minimumSimilarity = BlockProt.getDefaultConfig().getFriendSearchSimilarityPercentage(); - final var offlinePlayers = Bukkit.getOfflinePlayers(); - final var filterStream = StreamSupport.stream(spliterator(offlinePlayers, 0, offlinePlayers.length), true).filter(p -> { - // Filter all the players by search criteria. - // If the strings are similar by 30%, the strings are considered similar (imo) and should be added. - // If they're less than 30% similar, we should still check if it possibly contains the search criteria - // and still add that user. - if (p.getName() == null || p.getUniqueId().equals(player.getUniqueId())) return false; - else if (p.getName().contains(searchQuery)) return true; - else return compareStrings(p.getName(), searchQuery) >= minimumSimilarity; - }).sequential().filter(p -> { - // We need to convert it back to a sequential list since NBT can only be accessed from the main thread. - return !handler.containsFriend(p.getUniqueId().toString()); - }); - - state.friendResultCache.clear(); - if (state.friendSearchState == InventoryState.FriendSearchState.FRIEND_SEARCH && state.getBlock() != null) { - // Allow integrations to additionally filter friends. - var filtered = filterStream - .filter(f -> PluginIntegration.filterFriendByUuidForAll(f.getUniqueId(), player, state.getBlock())) - .toList(); - state.friendResultCache.addAll(filtered); - } else { - state.friendResultCache.addAll(filterStream.toList()); + updateTask = Bukkit.getScheduler().runTaskTimer(BlockProt.getInstance(), new ResultUpdateTask(state), 0, 1); + loadTask = Bukkit.getScheduler().runTaskAsynchronously(BlockProt.getInstance(), new AsyncResultLoadTask(state, player, searchQuery)); + + for (int i = 0; i < maxResults; i++) { + this.setItemStack(i, Material.SKELETON_SKULL, "Loading..."); } + setBackButton(); + return inventory; + } + + /** This task is responsible for taking available results and inserting it into the inventory */ + private class ResultUpdateTask implements Runnable { + InventoryState state; + int playersIndex = 0; - // Finally, construct the inventory with all the potential friends. - // To not delay when the inventory opens, we'll asynchronously get the items after - // the inventory has been opened and later add them to the inventory. In the meantime, - // we'll show the same amount of skeleton heads. - final var finalList = state.friendResultCache; - final int maxPlayers = Math.min(finalList.size(), InventoryConstants.tripleLine - 1); - for (int i = 0; i < maxPlayers; i++) { - this.setItemStack(i, Material.SKELETON_SKULL, finalList.get(i).getName()); + ResultUpdateTask(@NotNull InventoryState state) { + this.state = state; } - Bukkit.getScheduler().runTaskAsynchronously(BlockProt.getInstance(), () -> { - // Only show the 9 * 3 - 1 most relevant players. Don't show any extra. - int playersIndex = 0; - while (playersIndex < maxPlayers && playersIndex < finalList.size()) { - // Only add to the inventory if this is not a friend (yet) - var profile = finalList.get(playersIndex).getPlayerProfile(); - try { - profile = profile.update().get(); - } catch (Exception e) { - BlockProt.getInstance().getLogger().warning("Failed to update PlayerProfile: " + e.getMessage()); + + @Override + public void run() { + final var scheduler = Bukkit.getScheduler(); + if (!scheduler.isQueued(loadTask.getTaskId()) && !scheduler.isCurrentlyRunning(loadTask.getTaskId()) && resultQueue.isEmpty()) { + if (playersIndex == 0) { + // If the task has stopped running and there are no results, clear the inventory + for (int i = 0; i < maxResults; i++) { + inventory.clear(i); + } + } + loadTask.cancel(); + updateTask.cancel(); + } + + Profile profile; + while ((profile = resultQueue.poll()) != null && playersIndex < maxResults) { + // Clear all the skeleton skulls named "Loading" + if (playersIndex == 0) { + for (int i = 0; i < maxResults; i++) { + inventory.clear(i); + } } - setPlayerSkull(playersIndex, profile); - playersIndex++; + state.friendResultCache.add(profile.getUniqueId()); + + setPlayerSkull(playersIndex, Bukkit.getServer().createPlayerProfile(profile.getUniqueId(), profile.getName())); + ++playersIndex; } - }); - setBackButton(); - return inventory; + + // If we hit the maximum amount of results we can just tell the task to stop, and cancel ourselves. + if (playersIndex == maxResults) { + loadTask.cancel(); + updateTask.cancel(); + } + } + } + + /** This task asynchronously loads all possible players and filters them based on the search criteria. + * It then adds every possible result to a queue that the {@link ResultUpdateTask} then consumes. */ + private class AsyncResultLoadTask implements Runnable { + InventoryState state; + Player player; + String searchQuery; + + AsyncResultLoadTask(@NotNull InventoryState state, @NotNull Player player, @NotNull String searchQuery) { + this.state = state; + this.player = player; + this.searchQuery = searchQuery; + } + + @Override + public void run() { + double minimumSimilarity = BlockProt.getDefaultConfig().getFriendSearchSimilarityPercentage(); + final var offlinePlayers = Bukkit.getOfflinePlayers(); + + final var stream = Arrays.stream(offlinePlayers) + .map(OfflinePlayer::getUniqueId) + // Other plugins/mods might use other UUID versions for NPCs or other players. + .filter(uuid -> uuid.version() == 3 || uuid.version() == 4); + + try { + var filterStream = BlockProt.getProfileService().findAllByUuid(stream.toList()).stream() + .filter(Objects::nonNull) + .filter(p -> p.getName() != null && !p.getUniqueId().equals(player.getUniqueId())) + .map(p -> new ImmutablePair<>(p, compareStrings(p.getName(), searchQuery))) + .filter(p -> p.right >= minimumSimilarity) + .sorted((a, b) -> b.right.compareTo(a.right)) + .map(p -> p.left); + + if (state.friendSearchState == InventoryState.FriendSearchState.FRIEND_SEARCH && state.getBlock() != null) { + filterStream = filterStream + .filter(f -> PluginIntegration.filterFriendByUuidForAll(f.getUniqueId(), player, state.getBlock())); + } + + filterStream.limit(maxResults).forEach(resultQueue::add); + } catch (Exception e) { + BlockProt.getInstance().getLogger().warning("Failed to search and filter players during friend search: " + e.getMessage()); + } + } } } diff --git a/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/InventoryState.java b/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/InventoryState.java index 800fd5e6..de9827b9 100644 --- a/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/InventoryState.java +++ b/spigot/src/main/java/de/sean/blockprot/bukkit/inventories/InventoryState.java @@ -19,7 +19,6 @@ package de.sean.blockprot.bukkit.inventories; import de.sean.blockprot.bukkit.events.BlockAccessMenuEvent; -import org.bukkit.OfflinePlayer; import org.bukkit.block.Block; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -44,10 +43,10 @@ public final class InventoryState { /** * A local cache of offline players for this state. * - * @since 0.4.7 + * @since 1.1.16 */ @NotNull - public final ArrayList friendResultCache = new ArrayList<>(); + public final ArrayList friendResultCache = new ArrayList<>(); @Nullable private final Block block; @@ -75,10 +74,10 @@ public final class InventoryState { /** * The friend we currently want to modify with {@link FriendDetailInventory}. * - * @since 0.4.7 + * @since 1.1.16 */ @Nullable - public OfflinePlayer currentFriend = null; + public UUID currentFriend = null; /** * The current cached menu permissions for this player. diff --git a/spigot/src/main/java/de/sean/blockprot/bukkit/listeners/BlockEventListener.java b/spigot/src/main/java/de/sean/blockprot/bukkit/listeners/BlockEventListener.java index 78499d8f..05923838 100644 --- a/spigot/src/main/java/de/sean/blockprot/bukkit/listeners/BlockEventListener.java +++ b/spigot/src/main/java/de/sean/blockprot/bukkit/listeners/BlockEventListener.java @@ -30,9 +30,6 @@ import de.sean.blockprot.bukkit.util.BlockUtil; import de.sean.blockprot.nbt.LockReturnValue; import de.tr7zw.changeme.nbtapi.NBT; -import de.tr7zw.changeme.nbtapi.NBTCompound; -import de.tr7zw.changeme.nbtapi.NBTItem; -import de.tr7zw.changeme.nbtapi.NBTTileEntity; import de.tr7zw.changeme.nbtapi.utils.MinecraftVersion; import net.md_5.bungee.api.ChatMessageType; import net.md_5.bungee.api.chat.TextComponent; diff --git a/spigot/src/main/java/de/sean/blockprot/bukkit/listeners/HopperEventListener.java b/spigot/src/main/java/de/sean/blockprot/bukkit/listeners/HopperEventListener.java index e98986fe..e256a471 100644 --- a/spigot/src/main/java/de/sean/blockprot/bukkit/listeners/HopperEventListener.java +++ b/spigot/src/main/java/de/sean/blockprot/bukkit/listeners/HopperEventListener.java @@ -20,7 +20,6 @@ import de.sean.blockprot.bukkit.BlockProt; import de.sean.blockprot.bukkit.nbt.BlockNBTHandler; -import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.block.Container; import org.bukkit.block.DoubleChest; diff --git a/spigot/src/main/java/de/sean/blockprot/bukkit/listeners/InventoryEventListener.java b/spigot/src/main/java/de/sean/blockprot/bukkit/listeners/InventoryEventListener.java index e56734f6..3c3ffb57 100644 --- a/spigot/src/main/java/de/sean/blockprot/bukkit/listeners/InventoryEventListener.java +++ b/spigot/src/main/java/de/sean/blockprot/bukkit/listeners/InventoryEventListener.java @@ -40,7 +40,6 @@ import org.bukkit.event.Listener; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryCloseEvent; -import org.bukkit.event.inventory.InventoryMoveItemEvent; import org.bukkit.event.inventory.InventoryOpenEvent; import org.bukkit.event.player.PlayerTakeLecternBookEvent; import org.bukkit.inventory.BlockInventoryHolder; diff --git a/spigot/src/main/java/de/sean/blockprot/bukkit/nbt/FriendHandler.java b/spigot/src/main/java/de/sean/blockprot/bukkit/nbt/FriendHandler.java index f3fe0139..204e0f1d 100644 --- a/spigot/src/main/java/de/sean/blockprot/bukkit/nbt/FriendHandler.java +++ b/spigot/src/main/java/de/sean/blockprot/bukkit/nbt/FriendHandler.java @@ -57,11 +57,10 @@ public String getName() { /** * A single friend handler can represent the whole player-base, giving access * to anyone on the server with specific access flags. We represent everyone by - * using a zeroed UUID. Nobody seems to have this UUID: - * https://namemc.com/profile/00000000-0000-0000-0000-000000000000?q=00000000-0000-0000-0000-000000000000 + * using an invalid UUID. */ public boolean doesRepresentPublic() { - return getName().equals(FriendSupportingHandler.zeroedUuid); + return getName().equals(FriendSupportingHandler.publicUuid.toString()); } /** diff --git a/spigot/src/main/java/de/sean/blockprot/bukkit/nbt/FriendSupportingHandler.java b/spigot/src/main/java/de/sean/blockprot/bukkit/nbt/FriendSupportingHandler.java index 5efd443e..23e508b4 100644 --- a/spigot/src/main/java/de/sean/blockprot/bukkit/nbt/FriendSupportingHandler.java +++ b/spigot/src/main/java/de/sean/blockprot/bukkit/nbt/FriendSupportingHandler.java @@ -24,10 +24,7 @@ import org.bukkit.OfflinePlayer; import org.jetbrains.annotations.NotNull; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.UUID; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -39,7 +36,8 @@ public abstract class FriendSupportingHandler extends NBTHandler { private final @NotNull String friendNbtKey; - public static final String zeroedUuid = new UUID(0, 0).toString(); + /** Mojang recently started requiring zeroed UUIDs to *not* be used, so we instead use an invalid UUID */ + public static final UUID publicUuid = new UUID(~0, ~0); public FriendSupportingHandler(@NotNull String friendNbtKey) { this.friendNbtKey = friendNbtKey; @@ -72,7 +70,10 @@ public Stream getFriendsStream() { return compound .getKeys() .stream() - .map((k) -> new FriendHandler(compound.getCompound(k))); + .map((k) -> new FriendHandler(compound.getCompound(k))) + // This is a weird Comparator, but it essentially just guarantees that the entry where + // getName().equals(publicUuid.toString()) is at the front of the stream. + .sorted((a, b) -> a.doesRepresentPublic() ? -1 : 1); } /** @@ -92,8 +93,10 @@ public List getFriends() { public List getFriendsAsPlayers() { return this.getFriendsStream() .flatMap(f -> { - if (f.getName().equals(zeroedUuid)) { - return Arrays.stream(Bukkit.getOfflinePlayers()); + if (f.doesRepresentPublic()) { + return Arrays.stream(Bukkit.getOfflinePlayers()) + // Other plugins/mods might use other UUID versions for NPCs or other players. + .filter(p -> p.getUniqueId().version() == 3 || p.getUniqueId().version() == 4); } else { return Stream.of(f); } @@ -129,10 +132,10 @@ public void setFriends(@NotNull final List friends) { @NotNull public Optional getFriend(@NotNull final String id) { return getFriendsStream() - .filter(f -> f.getName().equals(id) || f.getName().equals(zeroedUuid)) + .filter(f -> f.getName().equals(id) || f.doesRepresentPublic()) // This is a weird Comparator, but it essentially just guarantees that the entry where - // getName().equals(zeroedUuid) is at the end of the stream. - .min((a, b) -> a.getName().equals(zeroedUuid) ? 1 : -1); + // getName().equals(publicUuid.toString()) is at the end of the stream. + .min((a, b) -> a.doesRepresentPublic() ? 1 : -1); } /** @@ -154,7 +157,7 @@ public void addFriend(@NotNull final FriendHandler friend) { * @since 1.1.0 */ public void addEveryoneAsFriend() { - addFriend(zeroedUuid); + addFriend(publicUuid.toString()); } /** diff --git a/spigot/src/main/java/de/sean/blockprot/bukkit/nbt/PlayerSettingsHandler.java b/spigot/src/main/java/de/sean/blockprot/bukkit/nbt/PlayerSettingsHandler.java index de9f3cbf..a469f0ed 100644 --- a/spigot/src/main/java/de/sean/blockprot/bukkit/nbt/PlayerSettingsHandler.java +++ b/spigot/src/main/java/de/sean/blockprot/bukkit/nbt/PlayerSettingsHandler.java @@ -30,6 +30,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.UUID; /** * A simple handler to get a player's BlockProt settings. @@ -126,20 +127,32 @@ protected void preFriendReadCallback() { * searched for. */ public List getSearchHistory() { - if (!container.hasKey(PLAYER_SEARCH_HISTORY)) return new ArrayList<>(); + if (!container.hasTag(PLAYER_SEARCH_HISTORY)) return new ArrayList<>(); else { return BlockProtUtil .parseStringList(container.getString(PLAYER_SEARCH_HISTORY)); } } + /** + * Clears the search history by removing the list completely. + * + * @since 1.1.16 + */ + public void clearSearchHistory() { + if (container.hasTag(PLAYER_SEARCH_HISTORY)) { + container.removeKey(PLAYER_SEARCH_HISTORY); + } + } + /** * Add a player to the search history. * * @param player The player to add. */ + @Deprecated public void addPlayerToSearchHistory(@NotNull final OfflinePlayer player) { - this.addPlayerToSearchHistory(player.getUniqueId().toString()); + this.addPlayerToSearchHistory(player.getUniqueId()); } /** @@ -147,15 +160,26 @@ public void addPlayerToSearchHistory(@NotNull final OfflinePlayer player) { * * @param playerUuid The player UUID to add. */ + @Deprecated public void addPlayerToSearchHistory(@NotNull final String playerUuid) { + this.addPlayerToSearchHistory(UUID.fromString(playerUuid)); + } + + /** + * Add a player to the search history. + * + * @param player The player to add. + * @since 1.1.16 + */ + public void addPlayerToSearchHistory(@NotNull final UUID player) { List history = getSearchHistory(); - if (!history.contains(playerUuid)) { + if (!history.contains(player.toString())) { // We want the list to not be bigger than MAX_HISTORY_SIZE, // therefore we remove the first entry if we would exceed that size. if (history.size() == MAX_HISTORY_SIZE) { history.remove(0); } - history.add(playerUuid); + history.add(player.toString()); container.setString(PLAYER_SEARCH_HISTORY, history.toString()); } }