Skip to content
This repository was archived by the owner on Dec 21, 2024. It is now read-only.
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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();
}
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Added PlayerProfile support by egg82 · Pull Request #16 · PaperMC/PaperLib · GitHub
Skip to content
This repository was archived by the owner on Dec 21, 2024. It is now read-only.
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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();
}
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Added PlayerProfile support by egg82 · Pull Request #16 · PaperMC/PaperLib · GitHub
Skip to content
This repository was archived by the owner on Dec 21, 2024. It is now read-only.
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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();
}
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Added PlayerProfile support by egg82 · Pull Request #16 · PaperMC/PaperLib · GitHub
Skip to content
This repository was archived by the owner on Dec 21, 2024. It is now read-only.
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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();
}
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Added PlayerProfile support by egg82 · Pull Request #16 · PaperMC/PaperLib · GitHub
Skip to content
This repository was archived by the owner on Dec 21, 2024. It is now read-only.
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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();
}
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Added PlayerProfile support by egg82 · Pull Request #16 · PaperMC/PaperLib · GitHub
Skip to content
This repository was archived by the owner on Dec 21, 2024. It is now read-only.
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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();
}
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Added PlayerProfile support by egg82 · Pull Request #16 · PaperMC/PaperLib · GitHub
Skip to content
This repository was archived by the owner on Dec 21, 2024. It is now read-only.
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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();
}
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Added PlayerProfile support by egg82 · Pull Request #16 · PaperMC/PaperLib · GitHub
Skip to content
This repository was archived by the owner on Dec 21, 2024. It is now read-only.
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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();
}