Skip to content

Commit

Permalink
Improve performance of friend search & manually cache player profiles
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
spnda committed Jun 10, 2024
1 parent d89e6b9 commit cc8d10c
Show file tree
Hide file tree
Showing 21 changed files with 460 additions and 168 deletions.
4 changes: 4 additions & 0 deletions spigot/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,15 @@ 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")

// 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")
Expand Down Expand Up @@ -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 {
Expand All @@ -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(
Expand Down
32 changes: 28 additions & 4 deletions spigot/src/main/java/de/sean/blockprot/bukkit/BlockProt.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -70,6 +70,11 @@ public final class BlockProt extends JavaPlugin {

private final ArrayList<PluginIntegration> integrations = new ArrayList<>();

@Nullable
private static SQLiteCache playerProfileCache = null;
@Nullable
private static ProfileService playerProfileService = null;

private Metrics metrics;

/**
Expand Down Expand Up @@ -109,10 +114,29 @@ public List<PluginIntegration> 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) {}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Profile> findAllByName(Iterable<String> names) throws IOException, InterruptedException {
final var profiles = this.resolver.findAllByName(names);
putAll(profiles);
return profiles;
}

@Override
public void findAllByName(Iterable<String> names, Predicate<Profile> 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<Profile> findAllByUuid(Iterable<UUID> uuids) {
final var map = getAllPresent(uuids);

if (map.isEmpty()) {
final var profiles = new ArrayList<Profile>();
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<UUID>();
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<UUID> uuids, Predicate<Profile> 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<Profile> iterable) {
this.cache.putAll(iterable);
}

@Nullable
@Override
public Profile getIfPresent(UUID uuid) {
return this.cache.getIfPresent(uuid);
}

@Override
public ImmutableMap<UUID, Profile> getAllPresent(Iterable<UUID> iterable) {
return this.cache.getAllPresent(iterable);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -59,7 +65,7 @@ public List<String> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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());
}
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit cc8d10c

Please sign in to comment.