Skip to content

Commit

Permalink
Implements ability to use faster player head gatherer (relates to #1646
Browse files Browse the repository at this point in the history
…). (#1679)

Adds 3 new BentoBox options:
- use-cache-server: option which allows to use mc-heads.net API for gathering player heads. It is a bit faster than Mojang API.
- heads-per-call: option which allows to specify how many heads will be requested at once per each API call.
- ticks-between-calls: option which allows to specify how many ticks should be waited until next API call.

All these options will allow much more faster player head gatherer.

Changes includes optimization for Mojang API too. For servers in online mode, HeadGetter will use Player UUID, instead of asking for UUID from API.
  • Loading branch information
BONNe authored Feb 16, 2021
1 parent 6316ca2 commit 79c3804
Show file tree
Hide file tree
Showing 3 changed files with 248 additions and 31 deletions.
88 changes: 88 additions & 0 deletions src/main/java/world/bentobox/bentobox/Settings.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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;
}
}
177 changes: 146 additions & 31 deletions src/main/java/world/bentobox/bentobox/util/heads/HeadGetter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<String, PanelItem> 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<UUID, String> 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());
}


Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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<UUID, String> 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
*
Expand All @@ -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 = "";
}
Expand Down
14 changes: 14 additions & 0 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 79c3804

Please sign in to comment.