diff --git a/src/main/java/world/bentobox/bentobox/Settings.java b/src/main/java/world/bentobox/bentobox/Settings.java index 360fd7a80..4f1f12ccd 100644 --- a/src/main/java/world/bentobox/bentobox/Settings.java +++ b/src/main/java/world/bentobox/bentobox/Settings.java @@ -138,12 +138,28 @@ public class Settings implements ConfigObject { @ConfigEntry(path = "panel.filler-material", since = "1.14.0") private Material panelFillerMaterial = Material.LIGHT_BLUE_STAINED_GLASS_PANE; + @ConfigComment("Toggle whether player head texture should be gathered from Mojang API or mc-heads.net cache server.") + @ConfigComment("Mojang API sometime may be slow and may limit requests to the player data, so this will allow to") + @ConfigComment("get player heads a bit faster then Mojang API.") + @ConfigEntry(path = "panel.use-cache-server", since = "1.16.0") + private boolean useCacheServer = false; + @ConfigComment("Defines how long player skin texture link is stored into local cache before it is requested again.") @ConfigComment("Defined value is in the minutes.") @ConfigComment("Value 0 will not clear cache until server restart.") @ConfigEntry(path = "panel.head-cache-time", since = "1.14.1") private long playerHeadCacheTime = 60; + @ConfigComment("Defines a number of player heads requested per tasks.") + @ConfigComment("Setting it too large may lead to temporarily being blocked from head gatherer API.") + @ConfigEntry(path = "panel.heads-per-call", since = "1.16.0") + private int headsPerCall = 9; + + @ConfigComment("Defines a number of ticks between each player head request task.") + @ConfigComment("Setting it too large may lead to temporarily being blocked from head gatherer API.") + @ConfigEntry(path = "panel.ticks-between-calls", since = "1.16.0", needsRestart = true) + private long ticksBetweenCalls = 10; + /* * Logs */ @@ -783,4 +799,76 @@ public void setPlayerHeadCacheTime(long playerHeadCacheTime) { this.playerHeadCacheTime = playerHeadCacheTime; } + + + /** + * Is use cache server boolean. + * + * @return the boolean + * @since 1.16.0 + */ + public boolean isUseCacheServer() + { + return useCacheServer; + } + + + /** + * Sets use cache server. + * + * @param useCacheServer the use cache server + * @since 1.16.0 + */ + public void setUseCacheServer(boolean useCacheServer) + { + this.useCacheServer = useCacheServer; + } + + + /** + * Gets heads per call. + * + * @return the heads per call + * @since 1.16.0 + */ + public int getHeadsPerCall() + { + return headsPerCall; + } + + + /** + * Sets heads per call. + * + * @param headsPerCall the heads per call + * @since 1.16.0 + */ + public void setHeadsPerCall(int headsPerCall) + { + this.headsPerCall = headsPerCall; + } + + + /** + * Gets ticks between calls. + * + * @return the ticks between calls + * @since 1.16.0 + */ + public long getTicksBetweenCalls() + { + return ticksBetweenCalls; + } + + + /** + * Sets ticks between calls. + * + * @param ticksBetweenCalls the ticks between calls + * @since 1.16.0 + */ + public void setTicksBetweenCalls(long ticksBetweenCalls) + { + this.ticksBetweenCalls = ticksBetweenCalls; + } } diff --git a/src/main/java/world/bentobox/bentobox/util/heads/HeadGetter.java b/src/main/java/world/bentobox/bentobox/util/heads/HeadGetter.java index 9ad31e557..e171b9c79 100644 --- a/src/main/java/world/bentobox/bentobox/util/heads/HeadGetter.java +++ b/src/main/java/world/bentobox/bentobox/util/heads/HeadGetter.java @@ -2,8 +2,8 @@ import java.io.BufferedReader; import java.io.InputStreamReader; +import java.net.HttpURLConnection; import java.net.URL; -import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -14,6 +14,7 @@ import java.util.stream.Collectors; import org.bukkit.Bukkit; +import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import com.google.gson.Gson; @@ -114,48 +115,96 @@ public static void addToCache(HeadCache cache) /** - * This is main task that runs once every 20 ticks and tries to get a player head. + * This is main task that runs once every Settings#ticksBetweenCalls ticks and tries to get + * Settings#headsPerCall player heads at once. + * * @since 1.14.1 */ private void runPlayerHeadGetter() { Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, () -> { synchronized (HeadGetter.names) { - if (!HeadGetter.names.isEmpty()) + int counter = 0; + + while (!HeadGetter.names.isEmpty() && counter < plugin.getSettings().getHeadsPerCall()) { Pair elementEntry = HeadGetter.names.poll(); - - // TODO: In theory BentoBox could use User instance to find existing user UUID's. - // It would avoid one API call. final String userName = elementEntry.getKey(); - // Use cached userId as userId will not change :) - UUID userId = HeadGetter.cachedHeads.containsKey(userName) ? - HeadGetter.cachedHeads.get(userName).getUserId() : - HeadGetter.getUserIdFromName(userName); + // Hmm, task in task in task. That is a weird structure. + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + // Check if we can get user Id. + UUID userId; + + if (HeadGetter.cachedHeads.containsKey(userName)) + { + // If cache contains userName, it means that it was already stored. + // We can reuse stored data, as they should not be changed. + userId = HeadGetter.cachedHeads.get(userName).getUserId(); + } + else if (Bukkit.getServer().getOnlineMode()) + { + // If server is in online mode we can relay that UUID is correct. + // So we use thing that is stored in BentoBox players data. + userId = plugin.getPlayers().getUUID(userName); + } + else + { + // Assign null for later check, as I do not want to write ifs inside + // previous 2 checks. + userId = null; + } - // Create new cache object. - HeadCache cache = new HeadCache(userName, - userId, - HeadGetter.getTextureFromUUID(userId)); + HeadCache cache; - // Save in cache - HeadGetter.cachedHeads.put(userName, cache); + if (plugin.getSettings().isUseCacheServer()) + { + // Cache server has an implementation to get a skin just from player name. + Pair playerSkin = HeadGetter.getTextureFromName(userName, userId); - // Tell requesters the head came in - if (HeadGetter.headRequesters.containsKey(userName)) - { - for (HeadRequester req : HeadGetter.headRequesters.get(userName)) + // Create new cache object. + cache = new HeadCache(userName, + playerSkin.getKey(), + playerSkin.getValue()); + } + else { - elementEntry.getValue().setHead(cache.getPlayerHead()); + if (userId == null) + { + // Use MojangAPI to get userId from userName. + userId = HeadGetter.getUserIdFromName(userName); + } + + // Create new cache object. + cache = new HeadCache(userName, + userId, + HeadGetter.getTextureFromUUID(userId)); + } - Bukkit.getServer().getScheduler().runTaskAsynchronously(this.plugin, - () -> req.setHead(elementEntry.getValue())); + // Save in cache + HeadGetter.cachedHeads.put(userName, cache); + + // Tell requesters the head came in, but only if the texture is usable. + if (cache.encodedTextureLink != null && HeadGetter.headRequesters.containsKey(userName)) + { + for (HeadRequester req : HeadGetter.headRequesters.get(userName)) + { + elementEntry.getValue().setHead(cache.getPlayerHead()); + + if (!plugin.isShutdown()) + { + // Do not run task if plugin is shutting down. + Bukkit.getScheduler().runTaskAsynchronously(this.plugin, + () -> req.setHead(elementEntry.getValue())); + } + } } - } + }); + + counter++; } } - }, 0L, 10L); + }, 0, plugin.getSettings().getTicksBetweenCalls()); } @@ -189,8 +238,7 @@ private static UUID getUserIdFromName(String name) { // UUID just looks more fancy :) String userIdString = jsonObject.get("id").toString(). replace("\"", ""). - replaceFirst("([0-9a-fA-F]{8})([0-9a-fA-F]{4})([0-9a-fA-F]{4})([0-9a-fA-F]{4})([0-9a-fA-F]+)", - "$1-$2-$3-$4-$5"); + replaceFirst("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", "$1-$2-$3-$4-$5"); userId = UUID.fromString(userIdString); } @@ -260,6 +308,70 @@ private static UUID getUserIdFromName(String name) { } + /** + * This method gets and returns base64 encoded link to player skin texture from mc-heads.net. + * It tries to use UUID if it is a valid, otherwise it uses given username. + * + * @param userName userName + * @param userId UUID for the user. + * @return Encoded player skin texture or null. + * @since 1.16.0 + */ + private static @NonNull Pair getTextureFromName(String userName, @Nullable UUID userId) { + try + { + Gson gsonReader = new Gson(); + + // Get user encoded texture value. + // mc-heads returns correct skin with providing just a name, unlike mojang api, which + // requires UUID. + JsonObject jsonObject = gsonReader.fromJson( + HeadGetter.getURLContent("https://mc-heads.net/minecraft/profile/" + (userId == null ? userName : userId.toString())), + JsonObject.class); + + /* + * Returned Json Object: + { + id: USER_ID, + name: USER_NAME, + properties: [ + { + name: "textures", + value: ENCODED_BASE64_TEXTURE + } + ] + } + */ + + String decodedTexture = ""; + + String userIdString = jsonObject.get("id").toString(). + replace("\"", ""). + replaceFirst("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", "$1-$2-$3-$4-$5"); + + for (JsonElement element : jsonObject.getAsJsonArray("properties")) + { + JsonObject object = element.getAsJsonObject(); + + if (object.has("name") && + object.get("name").getAsString().equals("textures")) + { + decodedTexture = object.get("value").getAsString(); + break; + } + } + + return new Pair<>(UUID.fromString(userIdString), decodedTexture); + } + catch (Exception ignored) + { + } + + // return random uuid and null, to assign some values for cache. + return new Pair<>(userId, null); + } + + /** * This method gets page content of requested url * @@ -270,12 +382,15 @@ private static UUID getUserIdFromName(String name) { private static String getURLContent(String requestedUrl) { String returnValue; - try (BufferedReader reader = new BufferedReader( - new InputStreamReader(new URL(requestedUrl).openStream(), StandardCharsets.UTF_8))) + try { - returnValue = reader.lines().collect(Collectors.joining()); + URL url = new URL(requestedUrl); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream())); + returnValue = br.lines().collect(Collectors.joining()); + br.close(); } - catch (Exception ignored) + catch (Exception e) { returnValue = ""; } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 5060f5839..37b6f67b2 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -82,11 +82,25 @@ panel: # Defines the Material of the item that fills the gaps (in the header, etc.) of most panels. # Added since 1.14.0. filler-material: LIGHT_BLUE_STAINED_GLASS_PANE + # Toggle whether player head texture should be gathered from Mojang API or mc-heads.net cache server. + # Mojang API sometime may be slow and may limit requests to the player data, so this will allow to + # get player heads a bit faster then Mojang API. + # Added since 1.16.0. + use-cache-server: true # Defines how long player skin texture link is stored into local cache before it is requested again. # Defined value is in the minutes. # Value 0 will not clear cache until server restart. # Added since 1.14.1. head-cache-time: 60 + # Defines a number of player heads requested per tasks. + # Setting it too large may lead to temporarily being blocked from head gatherer API. + # Added since 1.16.0. + heads-per-call: 9 + # Defines a number of ticks between each player head request task. + # Setting it too large may lead to temporarily being blocked from head gatherer API. + # Added since 1.16.0. + # /!\ In order to apply the changes made to this option, you must restart your server. Reloading BentoBox or the server won't work. + ticks-between-calls: 10 logs: # Toggle whether superflat chunks regeneration should be logged in the server logs or not. # It can be spammy if there are a lot of superflat chunks to regenerate.