Skip to content
This repository has been archived by the owner on Dec 21, 2024. It is now read-only.

Added PlayerProfile support #16

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/main/java/io/papermc/lib/PaperLib.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import io.papermc.lib.environments.PaperEnvironment;
import io.papermc.lib.environments.SpigotEnvironment;
import io.papermc.lib.features.blockstatesnapshot.BlockStateSnapshotResult;
import io.papermc.lib.features.playerprofile.PlayerInfo;
import java.io.IOException;
import java.util.UUID;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.World;
Expand Down Expand Up @@ -163,6 +166,34 @@ public static BlockStateSnapshotResult getBlockState(@Nonnull Block block, boole
return ENVIRONMENT.getBlockState(block, useSnapshot);
}

/**
* Gets information about a potentially offline player, such as their current name or UUID.
* Note: Calling this method may contact Mojang's API and will block the current thread with the web request if it does.
* Additionally, it's possible that the name or UUID returned is null and/or an IOException is thrown.
*
* @param playerName The real name of the player to get
* @return A PlayerInfo object containing information about a player
* @throws IOException Thrown when an IOException is encountered while querying the Mojang API.
*/
@Nonnull
public static PlayerInfo getPlayerInfo(@Nonnull String playerName) throws IOException {
return ENVIRONMENT.getPlayerInfo(playerName);
}

/**
* Gets information about a potentially offline player, such as their current name or UUID.
* Note: Calling this method may contact Mojang's API and will block the current thread with the web request if it does.
* Additionally, it's possible that the name or UUID returned is null and/or an IOException is thrown.
*
* @param playerUUID The UUID of the player to get
* @return A PlayerInfo object containing information about a player
* @throws IOException Thrown when an IOException is encountered while querying the Mojang API.
*/
@Nonnull
public static PlayerInfo getPlayerInfo(@Nonnull UUID playerUUID) throws IOException {
return ENVIRONMENT.getPlayerInfo(playerUUID);
}

/**
* Detects if the current MC version is at least the following version.
*
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/io/papermc/lib/environments/Environment.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
import io.papermc.lib.features.chunkisgenerated.ChunkIsGenerated;
import io.papermc.lib.features.chunkisgenerated.ChunkIsGeneratedApiExists;
import io.papermc.lib.features.chunkisgenerated.ChunkIsGeneratedUnknown;
import io.papermc.lib.features.playerprofile.BukkitPlayerInfo;
import io.papermc.lib.features.playerprofile.PaperPlayerInfo;
import io.papermc.lib.features.playerprofile.PlayerInfo;
import java.io.IOException;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.Location;
Expand All @@ -34,6 +39,7 @@ public abstract class Environment {
protected AsyncTeleport asyncTeleportHandler = new AsyncTeleportSync();
protected ChunkIsGenerated isGeneratedHandler = new ChunkIsGeneratedUnknown();
protected BlockStateSnapshot blockStateSnapshotHandler;
protected boolean hasPlayerProfile = true;

public Environment() {
Pattern versionPattern = Pattern.compile("\\(MC: (\\d)\\.(\\d+)\\.?(\\d+?)?\\)");
Expand Down Expand Up @@ -67,6 +73,12 @@ public Environment() {
} else {
blockStateSnapshotHandler = new BlockStateSnapshotNoOption();
}

try {
Class.forName("com.destroystokyo.paper.profile.PlayerProfile");
} catch (ClassNotFoundException ignored) {
hasPlayerProfile = false;
}
}

public abstract String getName();
Expand All @@ -87,6 +99,14 @@ public BlockStateSnapshotResult getBlockState(Block block, boolean useSnapshot)
return blockStateSnapshotHandler.getBlockState(block, useSnapshot);
}

public PlayerInfo getPlayerInfo(String playerName) throws IOException {
return (hasPlayerProfile) ? new PaperPlayerInfo(playerName) : new BukkitPlayerInfo(playerName);
}

public PlayerInfo getPlayerInfo(UUID playerUUID) throws IOException {
return (hasPlayerProfile) ? new PaperPlayerInfo(playerUUID) : new BukkitPlayerInfo(playerUUID);
}

public boolean isVersion(int minor) {
return isVersion(minor, 0);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package io.papermc.lib.features.playerprofile;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;

public class BukkitPlayerInfo implements PlayerInfo {
private UUID uuid;
private String name;

private static ConcurrentMap<UUID, String> uuidCache = new ConcurrentHashMap<>();
private static ConcurrentMap<String, UUID> nameCache = new ConcurrentHashMap<>();

private static final Object uuidCacheLock = new Object();
private static final Object nameCacheLock = new Object();

private static final JSONParser uuidParser = new JSONParser(); // Thread-safe access via synchronized lock
private static final JSONParser nameParser = new JSONParser(); // Thread-safe access via synchronized lock

public BukkitPlayerInfo(UUID uuid) throws IOException {
this.uuid = uuid;

Optional<String> name = Optional.ofNullable(uuidCache.getOrDefault(uuid, null));
if (!name.isPresent()) {
synchronized (uuidCacheLock) { // Synchronize for thread-safe JSONParser access + defeating potential race conditions causing multiple lookups
name = Optional.ofNullable(uuidCache.getOrDefault(uuid, null));
if (!name.isPresent()) {
name = Optional.ofNullable(nameExpensive(uuid));
name.ifPresent(v -> uuidCache.put(uuid, v));
}
}
}

this.name = name.orElse(null);
}

public BukkitPlayerInfo(String name) throws IOException {
this.name = name;

Optional<UUID> uuid = Optional.ofNullable(nameCache.getOrDefault(name, null));
if (!uuid.isPresent()) {
synchronized (nameCacheLock) { // Synchronize for thread-safe JSONParser access + defeating potential race conditions causing multiple lookups
uuid = Optional.ofNullable(nameCache.getOrDefault(name, null));
if (!uuid.isPresent()) {
uuid = Optional.ofNullable(uuidExpensive(name));
uuid.ifPresent(v -> nameCache.put(name, v));
}
}
}

this.uuid = uuid.orElse(null);
}

public UUID getUUID() { return uuid; }

public String getName() { return name; }

private static String nameExpensive(UUID uuid) throws IOException {
// Currently-online lookup
Player player = Bukkit.getPlayer(uuid);
if (player != null) {
nameCache.put(player.getName(), uuid);
return player.getName();
}

// Network lookup
HttpURLConnection conn = getConnection(new URL("https://api.mojang.com/user/profiles/" + uuid.toString().replace("-", "") + "/names"));

int code = conn.getResponseCode();
try (
InputStream in = (code == 200) ? conn.getInputStream() : conn.getErrorStream(); // Ensure we always get some text
InputStreamReader reader = new InputStreamReader(in);
BufferedReader buffer = new BufferedReader(reader)
) {
StringBuilder builder = new StringBuilder();
String line;
while ((line = buffer.readLine()) != null) {
builder.append(line);
}

if (code == 200) {
JSONArray json = (JSONArray) nameParser.parse(builder.toString());
JSONObject last = (JSONObject) json.get(json.size() - 1);
String name = (String) last.get("name");

nameCache.put(name, uuid);
} else if (code == 204) {
// No data exists
return null;
}
} catch (ParseException ex) {
throw new IOException(ex.getMessage(), ex);
}

throw new IOException("Could not load player data from Mojang (rate-limited?)");
}

private static UUID uuidExpensive(String name) throws IOException {
// Currently-online lookup
Player player = Bukkit.getPlayer(name);
if (player != null) {
uuidCache.put(player.getUniqueId(), name);
return player.getUniqueId();
}

// Network lookup
HttpURLConnection conn = getConnection(new URL("https://api.mojang.com/users/profiles/minecraft/" + name));

int code = conn.getResponseCode();
try (
InputStream in = (code == 200) ? conn.getInputStream() : conn.getErrorStream(); // Ensure we always get some text
InputStreamReader reader = new InputStreamReader(in);
BufferedReader buffer = new BufferedReader(reader)
) {
StringBuilder builder = new StringBuilder();
String line;
while ((line = buffer.readLine()) != null) {
builder.append(line);
}

if (code == 200) {
JSONObject json = (JSONObject) uuidParser.parse(builder.toString());
UUID uuid = UUID.fromString(((String) json.get("id")).replaceFirst("(\\p{XDigit}{8})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}+)", "$1-$2-$3-$4-$5")); // Normalize UUID for fromString (expects dashes, Mojang returns non-dashed result)
name = (String) json.get("name");

uuidCache.put(uuid, name);
} else if (code == 204) {
// No data exists
return null;
}
} catch (ParseException ex) {
throw new IOException(ex.getMessage(), ex);
}

throw new IOException("Could not load player data from Mojang (rate-limited?)");
}

private static HttpURLConnection getConnection(URL url) throws IOException {
HttpURLConnection conn = getBaseConnection(url);
conn.setInstanceFollowRedirects(true);

int status;
boolean redirect;

do {
status = conn.getResponseCode();
redirect = status == HttpURLConnection.HTTP_MOVED_TEMP || status == HttpURLConnection.HTTP_MOVED_PERM || status == HttpURLConnection.HTTP_SEE_OTHER; // Follow redirects

if (redirect) {
// Set cookies on redirect and follow redirect URL
String newUrl = conn.getHeaderField("Location");
String cookies = conn.getHeaderField("Set-Cookie");

conn = getBaseConnection(new URL(newUrl));
conn.setRequestProperty("Cookie", cookies);
conn.addRequestProperty("Accept-Language", "en-US,en;q=0.8");
}
} while (redirect);

return conn;
}

private static HttpURLConnection getBaseConnection(URL url) throws IOException {
HttpURLConnection conn = (HttpURLConnection) url.openConnection();

// Set standard headers
conn.setRequestProperty("Accept", "application/json");
conn.setRequestProperty("Connection", "close");
conn.setRequestProperty("User-Agent", "PaperMC/PaperLib");
conn.setRequestMethod("GET");

return conn;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package io.papermc.lib.features.playerprofile;

import com.destroystokyo.paper.profile.PlayerProfile;
import java.io.IOException;
import java.util.Optional;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;

public class PaperPlayerInfo implements PlayerInfo {
private UUID uuid;
private String name;

public PaperPlayerInfo(UUID uuid) throws IOException {
this.uuid = uuid;

Optional<String> name = Optional.ofNullable(nameExpensive(uuid));
this.name = name.orElse(null);
}

public PaperPlayerInfo(String name) throws IOException {
this.name = name;

Optional<UUID> uuid = Optional.ofNullable(uuidExpensive(name));
this.uuid = uuid.orElse(null);
}

public UUID getUUID() { return uuid; }

public String getName() { return name; }

private static String nameExpensive(UUID uuid) throws IOException {
// Currently-online lookup
Player player = Bukkit.getPlayer(uuid);
if (player != null) {
return player.getName();
}

// Cached profile lookup
PlayerProfile profile = Bukkit.createProfile(uuid);
if ((profile.isComplete() || profile.completeFromCache()) && profile.getName() != null && profile.getId() != null) {
return profile.getName();
}

// Network lookup
if (profile.complete(false) && profile.getName() != null && profile.getId() != null) {
return profile.getName();
}

// Sorry, nada
throw new IOException("Could not load player data from Mojang (rate-limited?)");
}

private static UUID uuidExpensive(String name) throws IOException {
// Currently-online lookup
Player player = Bukkit.getPlayer(name);
if (player != null) {
return player.getUniqueId();
}

// Cached profile lookup
PlayerProfile profile = Bukkit.createProfile(name);
if ((profile.isComplete() || profile.completeFromCache()) && profile.getName() != null && profile.getId() != null) {
return profile.getId();
}

// Network lookup
if (profile.complete(false) && profile.getName() != null && profile.getId() != null) {
return profile.getId();
}

// Sorry, nada
throw new IOException("Could not load player data from Mojang (rate-limited?)");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.papermc.lib.features.playerprofile;

import java.util.UUID;
import javax.annotation.Nullable;

public interface PlayerInfo {
/**
* The name of the player.
*
* @return The name of the payer.
*/
@Nullable String getName();

/**
* The UUID of the player.
*
* @return The UUID of the player.
*/
@Nullable UUID getUUID();
}