#1002: Add Player Profile API
Slight changes may occur as this API is stabilized. This PR is based on work previously done by DerFrZocker in #938.
This commit is contained in:
parent
911875d4f8
commit
8f361ecec0
@ -19,10 +19,12 @@ import org.bukkit.Server;
|
|||||||
import org.bukkit.Statistic;
|
import org.bukkit.Statistic;
|
||||||
import org.bukkit.configuration.serialization.ConfigurationSerializable;
|
import org.bukkit.configuration.serialization.ConfigurationSerializable;
|
||||||
import org.bukkit.configuration.serialization.SerializableAs;
|
import org.bukkit.configuration.serialization.SerializableAs;
|
||||||
|
import org.bukkit.craftbukkit.profile.CraftPlayerProfile;
|
||||||
import org.bukkit.entity.EntityType;
|
import org.bukkit.entity.EntityType;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.metadata.MetadataValue;
|
import org.bukkit.metadata.MetadataValue;
|
||||||
import org.bukkit.plugin.Plugin;
|
import org.bukkit.plugin.Plugin;
|
||||||
|
import org.bukkit.profile.PlayerProfile;
|
||||||
|
|
||||||
@SerializableAs("Player")
|
@SerializableAs("Player")
|
||||||
public class CraftOfflinePlayer implements OfflinePlayer, ConfigurationSerializable {
|
public class CraftOfflinePlayer implements OfflinePlayer, ConfigurationSerializable {
|
||||||
@ -37,10 +39,6 @@ public class CraftOfflinePlayer implements OfflinePlayer, ConfigurationSerializa
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public GameProfile getProfile() {
|
|
||||||
return profile;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isOnline() {
|
public boolean isOnline() {
|
||||||
return getPlayer() != null;
|
return getPlayer() != null;
|
||||||
@ -74,6 +72,11 @@ public class CraftOfflinePlayer implements OfflinePlayer, ConfigurationSerializa
|
|||||||
return profile.getId();
|
return profile.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PlayerProfile getPlayerProfile() {
|
||||||
|
return new CraftPlayerProfile(profile);
|
||||||
|
}
|
||||||
|
|
||||||
public Server getServer() {
|
public Server getServer() {
|
||||||
return server;
|
return server;
|
||||||
}
|
}
|
||||||
|
@ -187,6 +187,7 @@ import org.bukkit.craftbukkit.metadata.EntityMetadataStore;
|
|||||||
import org.bukkit.craftbukkit.metadata.PlayerMetadataStore;
|
import org.bukkit.craftbukkit.metadata.PlayerMetadataStore;
|
||||||
import org.bukkit.craftbukkit.metadata.WorldMetadataStore;
|
import org.bukkit.craftbukkit.metadata.WorldMetadataStore;
|
||||||
import org.bukkit.craftbukkit.potion.CraftPotionBrewer;
|
import org.bukkit.craftbukkit.potion.CraftPotionBrewer;
|
||||||
|
import org.bukkit.craftbukkit.profile.CraftPlayerProfile;
|
||||||
import org.bukkit.craftbukkit.scheduler.CraftScheduler;
|
import org.bukkit.craftbukkit.scheduler.CraftScheduler;
|
||||||
import org.bukkit.craftbukkit.scoreboard.CraftScoreboardManager;
|
import org.bukkit.craftbukkit.scoreboard.CraftScoreboardManager;
|
||||||
import org.bukkit.craftbukkit.structure.CraftStructureManager;
|
import org.bukkit.craftbukkit.structure.CraftStructureManager;
|
||||||
@ -244,6 +245,7 @@ import org.bukkit.plugin.messaging.Messenger;
|
|||||||
import org.bukkit.plugin.messaging.StandardMessenger;
|
import org.bukkit.plugin.messaging.StandardMessenger;
|
||||||
import org.bukkit.potion.Potion;
|
import org.bukkit.potion.Potion;
|
||||||
import org.bukkit.potion.PotionEffectType;
|
import org.bukkit.potion.PotionEffectType;
|
||||||
|
import org.bukkit.profile.PlayerProfile;
|
||||||
import org.bukkit.scheduler.BukkitWorker;
|
import org.bukkit.scheduler.BukkitWorker;
|
||||||
import org.bukkit.structure.StructureManager;
|
import org.bukkit.structure.StructureManager;
|
||||||
import org.bukkit.util.StringUtil;
|
import org.bukkit.util.StringUtil;
|
||||||
@ -294,6 +296,7 @@ public final class CraftServer implements Server {
|
|||||||
|
|
||||||
static {
|
static {
|
||||||
ConfigurationSerialization.registerClass(CraftOfflinePlayer.class);
|
ConfigurationSerialization.registerClass(CraftOfflinePlayer.class);
|
||||||
|
ConfigurationSerialization.registerClass(CraftPlayerProfile.class);
|
||||||
CraftItemFactory.instance();
|
CraftItemFactory.instance();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1643,6 +1646,21 @@ public final class CraftServer implements Server {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PlayerProfile createPlayerProfile(UUID uniqueId, String name) {
|
||||||
|
return new CraftPlayerProfile(uniqueId, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PlayerProfile createPlayerProfile(UUID uniqueId) {
|
||||||
|
return new CraftPlayerProfile(uniqueId, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PlayerProfile createPlayerProfile(String name) {
|
||||||
|
return new CraftPlayerProfile(null, name);
|
||||||
|
}
|
||||||
|
|
||||||
public OfflinePlayer getOfflinePlayer(GameProfile profile) {
|
public OfflinePlayer getOfflinePlayer(GameProfile profile) {
|
||||||
OfflinePlayer player = new CraftOfflinePlayer(this, profile);
|
OfflinePlayer player = new CraftOfflinePlayer(this, profile);
|
||||||
offlinePlayers.put(profile.getId(), player);
|
offlinePlayers.put(profile.getId(), player);
|
||||||
|
@ -14,6 +14,8 @@ import org.bukkit.block.data.BlockData;
|
|||||||
import org.bukkit.block.data.Directional;
|
import org.bukkit.block.data.Directional;
|
||||||
import org.bukkit.block.data.Rotatable;
|
import org.bukkit.block.data.Rotatable;
|
||||||
import org.bukkit.craftbukkit.entity.CraftPlayer;
|
import org.bukkit.craftbukkit.entity.CraftPlayer;
|
||||||
|
import org.bukkit.craftbukkit.profile.CraftPlayerProfile;
|
||||||
|
import org.bukkit.profile.PlayerProfile;
|
||||||
|
|
||||||
public class CraftSkull extends CraftBlockEntityState<TileEntitySkull> implements Skull {
|
public class CraftSkull extends CraftBlockEntityState<TileEntitySkull> implements Skull {
|
||||||
|
|
||||||
@ -100,6 +102,24 @@ public class CraftSkull extends CraftBlockEntityState<TileEntitySkull> implement
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PlayerProfile getOwnerProfile() {
|
||||||
|
if (!hasOwner()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CraftPlayerProfile(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setOwnerProfile(PlayerProfile profile) {
|
||||||
|
if (profile == null) {
|
||||||
|
this.profile = null;
|
||||||
|
} else {
|
||||||
|
this.profile = CraftPlayerProfile.validateSkullProfile(((CraftPlayerProfile) profile).buildGameProfile());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BlockFace getRotation() {
|
public BlockFace getRotation() {
|
||||||
BlockData blockData = getBlockData();
|
BlockData blockData = getBlockData();
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
package org.bukkit.craftbukkit.configuration;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.bukkit.configuration.serialization.ConfigurationSerializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utilities related to the serialization and deserialization of {@link ConfigurationSerializable}s.
|
||||||
|
*/
|
||||||
|
public final class ConfigSerializationUtil {
|
||||||
|
|
||||||
|
public static String getString(Map<?, ?> map, String key, boolean nullable) {
|
||||||
|
return getObject(String.class, map, key, nullable);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static UUID getUuid(Map<?, ?> map, String key, boolean nullable) {
|
||||||
|
String uuidString = ConfigSerializationUtil.getString(map, key, nullable);
|
||||||
|
if (uuidString == null) return null;
|
||||||
|
return UUID.fromString(uuidString);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> T getObject(Class<T> clazz, Map<?, ?> map, String key, boolean nullable) {
|
||||||
|
final Object object = map.get(key);
|
||||||
|
if (clazz.isInstance(object)) {
|
||||||
|
return clazz.cast(object);
|
||||||
|
}
|
||||||
|
if (object == null) {
|
||||||
|
if (!nullable) {
|
||||||
|
throw new NoSuchElementException(map + " does not contain " + key);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException(key + "(" + object + ") is not a valid " + clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ConfigSerializationUtil() {
|
||||||
|
}
|
||||||
|
}
|
@ -119,6 +119,7 @@ import org.bukkit.craftbukkit.conversations.ConversationTracker;
|
|||||||
import org.bukkit.craftbukkit.inventory.CraftItemStack;
|
import org.bukkit.craftbukkit.inventory.CraftItemStack;
|
||||||
import org.bukkit.craftbukkit.map.CraftMapView;
|
import org.bukkit.craftbukkit.map.CraftMapView;
|
||||||
import org.bukkit.craftbukkit.map.RenderData;
|
import org.bukkit.craftbukkit.map.RenderData;
|
||||||
|
import org.bukkit.craftbukkit.profile.CraftPlayerProfile;
|
||||||
import org.bukkit.craftbukkit.scoreboard.CraftScoreboard;
|
import org.bukkit.craftbukkit.scoreboard.CraftScoreboard;
|
||||||
import org.bukkit.craftbukkit.util.CraftChatMessage;
|
import org.bukkit.craftbukkit.util.CraftChatMessage;
|
||||||
import org.bukkit.craftbukkit.util.CraftMagicNumbers;
|
import org.bukkit.craftbukkit.util.CraftMagicNumbers;
|
||||||
@ -139,6 +140,7 @@ import org.bukkit.map.MapView;
|
|||||||
import org.bukkit.metadata.MetadataValue;
|
import org.bukkit.metadata.MetadataValue;
|
||||||
import org.bukkit.plugin.Plugin;
|
import org.bukkit.plugin.Plugin;
|
||||||
import org.bukkit.plugin.messaging.StandardMessenger;
|
import org.bukkit.plugin.messaging.StandardMessenger;
|
||||||
|
import org.bukkit.profile.PlayerProfile;
|
||||||
import org.bukkit.scoreboard.Scoreboard;
|
import org.bukkit.scoreboard.Scoreboard;
|
||||||
|
|
||||||
@DelegateDeserialization(CraftOfflinePlayer.class)
|
@DelegateDeserialization(CraftOfflinePlayer.class)
|
||||||
@ -188,6 +190,11 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
|
|||||||
return server.getPlayer(getUniqueId()) != null;
|
return server.getPlayer(getUniqueId()) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PlayerProfile getPlayerProfile() {
|
||||||
|
return new CraftPlayerProfile(getProfile());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InetSocketAddress getAddress() {
|
public InetSocketAddress getAddress() {
|
||||||
if (getHandle().connection == null) return null;
|
if (getHandle().connection == null) return null;
|
||||||
|
@ -15,8 +15,10 @@ import org.bukkit.configuration.serialization.DelegateDeserialization;
|
|||||||
import org.bukkit.craftbukkit.entity.CraftPlayer;
|
import org.bukkit.craftbukkit.entity.CraftPlayer;
|
||||||
import org.bukkit.craftbukkit.inventory.CraftMetaItem.ItemMetaKey;
|
import org.bukkit.craftbukkit.inventory.CraftMetaItem.ItemMetaKey;
|
||||||
import org.bukkit.craftbukkit.inventory.CraftMetaItem.SerializableMeta;
|
import org.bukkit.craftbukkit.inventory.CraftMetaItem.SerializableMeta;
|
||||||
|
import org.bukkit.craftbukkit.profile.CraftPlayerProfile;
|
||||||
import org.bukkit.craftbukkit.util.CraftMagicNumbers;
|
import org.bukkit.craftbukkit.util.CraftMagicNumbers;
|
||||||
import org.bukkit.inventory.meta.SkullMeta;
|
import org.bukkit.inventory.meta.SkullMeta;
|
||||||
|
import org.bukkit.profile.PlayerProfile;
|
||||||
|
|
||||||
@DelegateDeserialization(SerializableMeta.class)
|
@DelegateDeserialization(SerializableMeta.class)
|
||||||
class CraftMetaSkull extends CraftMetaItem implements SkullMeta {
|
class CraftMetaSkull extends CraftMetaItem implements SkullMeta {
|
||||||
@ -52,9 +54,14 @@ class CraftMetaSkull extends CraftMetaItem implements SkullMeta {
|
|||||||
CraftMetaSkull(Map<String, Object> map) {
|
CraftMetaSkull(Map<String, Object> map) {
|
||||||
super(map);
|
super(map);
|
||||||
if (profile == null) {
|
if (profile == null) {
|
||||||
|
Object object = map.get(SKULL_OWNER.BUKKIT);
|
||||||
|
if (object instanceof PlayerProfile) {
|
||||||
|
setOwnerProfile((PlayerProfile) object);
|
||||||
|
} else {
|
||||||
setOwner(SerializableMeta.getString(map, SKULL_OWNER.BUKKIT, true));
|
setOwner(SerializableMeta.getString(map, SKULL_OWNER.BUKKIT, true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void deserializeInternal(NBTTagCompound tag, Object context) {
|
void deserializeInternal(NBTTagCompound tag, Object context) {
|
||||||
@ -187,6 +194,24 @@ class CraftMetaSkull extends CraftMetaItem implements SkullMeta {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PlayerProfile getOwnerProfile() {
|
||||||
|
if (!hasOwner()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CraftPlayerProfile(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setOwnerProfile(PlayerProfile profile) {
|
||||||
|
if (profile == null) {
|
||||||
|
setProfile(null);
|
||||||
|
} else {
|
||||||
|
setProfile(CraftPlayerProfile.validateSkullProfile(((CraftPlayerProfile) profile).buildGameProfile()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
int applyHash() {
|
int applyHash() {
|
||||||
final int original;
|
final int original;
|
||||||
@ -220,7 +245,7 @@ class CraftMetaSkull extends CraftMetaItem implements SkullMeta {
|
|||||||
Builder<String, Object> serialize(Builder<String, Object> builder) {
|
Builder<String, Object> serialize(Builder<String, Object> builder) {
|
||||||
super.serialize(builder);
|
super.serialize(builder);
|
||||||
if (hasOwner()) {
|
if (hasOwner()) {
|
||||||
return builder.put(SKULL_OWNER.BUKKIT, this.profile.getName());
|
return builder.put(SKULL_OWNER.BUKKIT, new CraftPlayerProfile(this.profile));
|
||||||
}
|
}
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,276 @@
|
|||||||
|
package org.bukkit.craftbukkit.profile;
|
||||||
|
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.google.common.collect.Iterables;
|
||||||
|
import com.mojang.authlib.GameProfile;
|
||||||
|
import com.mojang.authlib.properties.Property;
|
||||||
|
import com.mojang.authlib.properties.PropertyMap;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import net.minecraft.SystemUtils;
|
||||||
|
import net.minecraft.server.dedicated.DedicatedServer;
|
||||||
|
import org.apache.commons.lang.StringUtils;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.configuration.serialization.SerializableAs;
|
||||||
|
import org.bukkit.craftbukkit.CraftServer;
|
||||||
|
import org.bukkit.craftbukkit.configuration.ConfigSerializationUtil;
|
||||||
|
import org.bukkit.profile.PlayerProfile;
|
||||||
|
import org.bukkit.profile.PlayerTextures;
|
||||||
|
|
||||||
|
@SerializableAs("PlayerProfile")
|
||||||
|
public final class CraftPlayerProfile implements PlayerProfile {
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static GameProfile validateSkullProfile(@Nonnull GameProfile gameProfile) {
|
||||||
|
// The GameProfile needs to contain either both a uuid and textures, or a name.
|
||||||
|
// The GameProfile always has a name or a uuid, so checking if it has a name is sufficient.
|
||||||
|
boolean isValidSkullProfile = (gameProfile.getName() != null)
|
||||||
|
|| gameProfile.getProperties().containsKey(CraftPlayerTextures.PROPERTY_NAME);
|
||||||
|
Preconditions.checkArgument(isValidSkullProfile, "The skull profile is missing a name or textures!");
|
||||||
|
return gameProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static Property getProperty(@Nonnull GameProfile profile, String propertyName) {
|
||||||
|
return Iterables.getFirst(profile.getProperties().get(propertyName), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final UUID uniqueId;
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
private final PropertyMap properties = new PropertyMap();
|
||||||
|
private final CraftPlayerTextures textures = new CraftPlayerTextures(this);
|
||||||
|
|
||||||
|
public CraftPlayerProfile(UUID uniqueId, String name) {
|
||||||
|
Preconditions.checkArgument((uniqueId != null) || !StringUtils.isBlank(name), "uniqueId is null or name is blank");
|
||||||
|
this.uniqueId = uniqueId;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Map of properties of the given GameProfile is not immutable. This captures a snapshot of the properties of
|
||||||
|
// the given GameProfile at the time this CraftPlayerProfile is created.
|
||||||
|
public CraftPlayerProfile(@Nonnull GameProfile gameProfile) {
|
||||||
|
this(gameProfile.getId(), gameProfile.getName());
|
||||||
|
properties.putAll(gameProfile.getProperties());
|
||||||
|
}
|
||||||
|
|
||||||
|
private CraftPlayerProfile(@Nonnull CraftPlayerProfile other) {
|
||||||
|
this(other.uniqueId, other.name);
|
||||||
|
this.properties.putAll(other.properties);
|
||||||
|
this.textures.copyFrom(other.textures);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UUID getUniqueId() {
|
||||||
|
return uniqueId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
Property getProperty(String propertyName) {
|
||||||
|
return Iterables.getFirst(properties.get(propertyName), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setProperty(String propertyName, @Nullable Property property) {
|
||||||
|
// Assert: (property == null) || property.getName().equals(propertyName)
|
||||||
|
removeProperty(propertyName);
|
||||||
|
if (property != null) {
|
||||||
|
properties.put(property.getName(), property);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeProperty(String propertyName) {
|
||||||
|
properties.removeAll(propertyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
void rebuildDirtyProperties() {
|
||||||
|
textures.rebuildPropertyIfDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CraftPlayerTextures getTextures() {
|
||||||
|
return textures;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTextures(@Nullable PlayerTextures textures) {
|
||||||
|
if (textures == null) {
|
||||||
|
this.textures.clear();
|
||||||
|
} else {
|
||||||
|
this.textures.copyFrom(textures);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isComplete() {
|
||||||
|
return (uniqueId != null) && (name != null) && !textures.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<PlayerProfile> update() {
|
||||||
|
return CompletableFuture.supplyAsync(this::getUpdatedProfile, SystemUtils.backgroundExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
private CraftPlayerProfile getUpdatedProfile() {
|
||||||
|
DedicatedServer server = ((CraftServer) Bukkit.getServer()).getServer();
|
||||||
|
GameProfile profile = this.buildGameProfile();
|
||||||
|
|
||||||
|
// If missing, look up the uuid by name:
|
||||||
|
if (profile.getId() == null) {
|
||||||
|
profile = server.getProfileCache().get(profile.getName()).orElse(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up properties such as the textures:
|
||||||
|
if (profile.getId() != null) {
|
||||||
|
GameProfile newProfile = server.getSessionService().fillProfileProperties(profile, true);
|
||||||
|
if (newProfile != null) {
|
||||||
|
profile = newProfile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CraftPlayerProfile(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This always returns a new GameProfile instance to ensure that property changes to the original or previously
|
||||||
|
// built GameProfiles don't affect the use of this profile in other contexts.
|
||||||
|
@Nonnull
|
||||||
|
public GameProfile buildGameProfile() {
|
||||||
|
rebuildDirtyProperties();
|
||||||
|
GameProfile profile = new GameProfile(uniqueId, name);
|
||||||
|
profile.getProperties().putAll(properties);
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
rebuildDirtyProperties();
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
builder.append("CraftPlayerProfile [uniqueId=");
|
||||||
|
builder.append(uniqueId);
|
||||||
|
builder.append(", name=");
|
||||||
|
builder.append(name);
|
||||||
|
builder.append(", properties=");
|
||||||
|
builder.append(toString(properties));
|
||||||
|
builder.append("]");
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String toString(@Nonnull PropertyMap propertyMap) {
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
builder.append("{");
|
||||||
|
propertyMap.asMap().forEach((propertyName, properties) -> {
|
||||||
|
builder.append(propertyName);
|
||||||
|
builder.append("=");
|
||||||
|
builder.append(properties.stream().map(CraftProfileProperty::toString).collect(Collectors.joining(",", "[", "]")));
|
||||||
|
});
|
||||||
|
builder.append("}");
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) return true;
|
||||||
|
if (!(obj instanceof CraftPlayerProfile)) return false;
|
||||||
|
CraftPlayerProfile other = (CraftPlayerProfile) obj;
|
||||||
|
if (!Objects.equals(uniqueId, other.uniqueId)) return false;
|
||||||
|
if (!Objects.equals(name, other.name)) return false;
|
||||||
|
|
||||||
|
rebuildDirtyProperties();
|
||||||
|
other.rebuildDirtyProperties();
|
||||||
|
if (!equals(properties, other.properties)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean equals(@Nonnull PropertyMap propertyMap, @Nonnull PropertyMap other) {
|
||||||
|
if (propertyMap.size() != other.size()) return false;
|
||||||
|
// We take the order of properties into account here, because it is
|
||||||
|
// also relevant in the serialized and NBT forms of GameProfiles.
|
||||||
|
Iterator<Property> iterator1 = propertyMap.values().iterator();
|
||||||
|
Iterator<Property> iterator2 = other.values().iterator();
|
||||||
|
while (iterator1.hasNext()) {
|
||||||
|
if (!iterator2.hasNext()) return false;
|
||||||
|
Property property1 = iterator1.next();
|
||||||
|
Property property2 = iterator2.next();
|
||||||
|
if (!CraftProfileProperty.equals(property1, property2)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !iterator2.hasNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
rebuildDirtyProperties();
|
||||||
|
int result = 1;
|
||||||
|
result = 31 * result + Objects.hashCode(uniqueId);
|
||||||
|
result = 31 * result + Objects.hashCode(name);
|
||||||
|
result = 31 * result + hashCode(properties);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int hashCode(PropertyMap propertyMap) {
|
||||||
|
int result = 1;
|
||||||
|
for (Property property : propertyMap.values()) {
|
||||||
|
result = 31 * result + CraftProfileProperty.hashCode(property);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CraftPlayerProfile clone() {
|
||||||
|
return new CraftPlayerProfile(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> serialize() {
|
||||||
|
Map<String, Object> map = new LinkedHashMap<>();
|
||||||
|
if (uniqueId != null) {
|
||||||
|
map.put("uniqueId", uniqueId.toString());
|
||||||
|
}
|
||||||
|
if (name != null) {
|
||||||
|
map.put("name", name);
|
||||||
|
}
|
||||||
|
rebuildDirtyProperties();
|
||||||
|
if (!properties.isEmpty()) {
|
||||||
|
List<Object> propertiesData = new ArrayList<>();
|
||||||
|
properties.forEach((propertyName, property) -> {
|
||||||
|
propertiesData.add(CraftProfileProperty.serialize(property));
|
||||||
|
});
|
||||||
|
map.put("properties", propertiesData);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CraftPlayerProfile deserialize(Map<String, Object> map) {
|
||||||
|
UUID uniqueId = ConfigSerializationUtil.getUuid(map, "uniqueId", true);
|
||||||
|
String name = ConfigSerializationUtil.getString(map, "name", true);
|
||||||
|
|
||||||
|
// This also validates the deserialized unique id and name (ensures that not both are null):
|
||||||
|
CraftPlayerProfile profile = new CraftPlayerProfile(uniqueId, name);
|
||||||
|
|
||||||
|
if (map.containsKey("properties")) {
|
||||||
|
for (Object propertyData : (List<?>) map.get("properties")) {
|
||||||
|
if (!(propertyData instanceof Map)) {
|
||||||
|
throw new IllegalArgumentException("Property data (" + propertyData + ") is not a valid Map");
|
||||||
|
}
|
||||||
|
Property property = CraftProfileProperty.deserialize((Map<?, ?>) propertyData);
|
||||||
|
profile.properties.put(property.getName(), property);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,317 @@
|
|||||||
|
package org.bukkit.craftbukkit.profile;
|
||||||
|
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonPrimitive;
|
||||||
|
import com.mojang.authlib.minecraft.MinecraftProfileTexture;
|
||||||
|
import com.mojang.authlib.properties.Property;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import org.bukkit.craftbukkit.util.JsonHelper;
|
||||||
|
import org.bukkit.profile.PlayerTextures;
|
||||||
|
|
||||||
|
final class CraftPlayerTextures implements PlayerTextures {
|
||||||
|
|
||||||
|
static final String PROPERTY_NAME = "textures";
|
||||||
|
private static final String MINECRAFT_HOST = "textures.minecraft.net";
|
||||||
|
private static final String MINECRAFT_PATH = "/texture/";
|
||||||
|
|
||||||
|
private static void validateTextureUrl(@Nullable URL url) {
|
||||||
|
// Null represents an unset texture and is therefore valid.
|
||||||
|
if (url == null) return;
|
||||||
|
|
||||||
|
Preconditions.checkArgument(url.getHost().equals(MINECRAFT_HOST), "Expected host '%s' but got '%s'", MINECRAFT_HOST, url.getHost());
|
||||||
|
Preconditions.checkArgument(url.getPath().startsWith(MINECRAFT_PATH), "Expected path starting with '%s' but got '%s", MINECRAFT_PATH, url.getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static URL parseUrl(@Nullable String urlString) {
|
||||||
|
if (urlString == null) return null;
|
||||||
|
try {
|
||||||
|
return new URL(urlString);
|
||||||
|
} catch (MalformedURLException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static SkinModel parseSkinModel(@Nullable String skinModelName) {
|
||||||
|
if (skinModelName == null) return null;
|
||||||
|
try {
|
||||||
|
return SkinModel.valueOf(skinModelName.toUpperCase(Locale.ROOT));
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final CraftPlayerProfile profile;
|
||||||
|
|
||||||
|
// The textures data is loaded lazily:
|
||||||
|
private boolean loaded = false;
|
||||||
|
private JsonObject data; // Immutable contents (only read)
|
||||||
|
private long timestamp;
|
||||||
|
|
||||||
|
// Lazily decoded textures data that can subsequently be overwritten:
|
||||||
|
private URL skin;
|
||||||
|
private SkinModel skinModel = SkinModel.CLASSIC;
|
||||||
|
private URL cape;
|
||||||
|
|
||||||
|
// Dirty: Indicates a change that requires a rebuild of the property.
|
||||||
|
// This also indicates an invalidation of any previously present textures data that is specific to official
|
||||||
|
// GameProfiles, such as the property signature, timestamp, profileId, playerName, etc.: Any modifications by
|
||||||
|
// plugins that affect the textures property immediately invalidate all attributes that are specific to official
|
||||||
|
// GameProfiles (even if these modifications are later reverted).
|
||||||
|
private boolean dirty = false;
|
||||||
|
|
||||||
|
CraftPlayerTextures(@Nonnull CraftPlayerProfile profile) {
|
||||||
|
this.profile = profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
void copyFrom(@Nonnull PlayerTextures other) {
|
||||||
|
if (other == this) return;
|
||||||
|
Preconditions.checkArgument(other instanceof CraftPlayerTextures, "Expecting CraftPlayerTextures, got %s", other.getClass().getName());
|
||||||
|
CraftPlayerTextures otherTextures = (CraftPlayerTextures) other;
|
||||||
|
clear();
|
||||||
|
Property texturesProperty = otherTextures.getProperty();
|
||||||
|
profile.setProperty(PROPERTY_NAME, texturesProperty);
|
||||||
|
if (texturesProperty != null
|
||||||
|
&& (!Objects.equals(profile.getUniqueId(), otherTextures.profile.getUniqueId())
|
||||||
|
|| !Objects.equals(profile.getName(), otherTextures.profile.getName()))) {
|
||||||
|
// We might need to rebuild the textures property for this profile:
|
||||||
|
// TODO Only rebuild if the textures property actually stores an incompatible profileId/playerName?
|
||||||
|
ensureLoaded();
|
||||||
|
markDirty();
|
||||||
|
rebuildPropertyIfDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureLoaded() {
|
||||||
|
if (loaded) return;
|
||||||
|
loaded = true;
|
||||||
|
|
||||||
|
Property property = getProperty();
|
||||||
|
if (property == null) return;
|
||||||
|
|
||||||
|
data = CraftProfileProperty.decodePropertyValue(property.getValue());
|
||||||
|
if (data != null) {
|
||||||
|
JsonObject texturesMap = JsonHelper.getObjectOrNull(data, "textures");
|
||||||
|
loadSkin(texturesMap);
|
||||||
|
loadCape(texturesMap);
|
||||||
|
loadTimestamp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadSkin(@Nullable JsonObject texturesMap) {
|
||||||
|
if (texturesMap == null) return;
|
||||||
|
JsonObject texture = JsonHelper.getObjectOrNull(texturesMap, MinecraftProfileTexture.Type.SKIN.name());
|
||||||
|
if (texture == null) return;
|
||||||
|
|
||||||
|
String skinUrlString = JsonHelper.getStringOrNull(texture, "url");
|
||||||
|
this.skin = parseUrl(skinUrlString);
|
||||||
|
this.skinModel = loadSkinModel(texture);
|
||||||
|
|
||||||
|
// Special case: If a skin is present, but no skin model, we use the default classic skin model.
|
||||||
|
if (skinModel == null && skin != null) {
|
||||||
|
skinModel = SkinModel.CLASSIC;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static SkinModel loadSkinModel(@Nullable JsonObject texture) {
|
||||||
|
if (texture == null) return null;
|
||||||
|
JsonObject metadata = JsonHelper.getObjectOrNull(texture, "metadata");
|
||||||
|
if (metadata == null) return null;
|
||||||
|
|
||||||
|
String skinModelName = JsonHelper.getStringOrNull(metadata, "model");
|
||||||
|
return parseSkinModel(skinModelName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadCape(@Nullable JsonObject texturesMap) {
|
||||||
|
if (texturesMap == null) return;
|
||||||
|
JsonObject texture = JsonHelper.getObjectOrNull(texturesMap, MinecraftProfileTexture.Type.CAPE.name());
|
||||||
|
if (texture == null) return;
|
||||||
|
|
||||||
|
String skinUrlString = JsonHelper.getStringOrNull(texture, "url");
|
||||||
|
this.cape = parseUrl(skinUrlString);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadTimestamp() {
|
||||||
|
if (data == null) return;
|
||||||
|
JsonPrimitive timestamp = JsonHelper.getPrimitiveOrNull(data, "timestamp");
|
||||||
|
if (timestamp == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.timestamp = timestamp.getAsLong();
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markDirty() {
|
||||||
|
dirty = true;
|
||||||
|
|
||||||
|
// Clear any cached but no longer valid data:
|
||||||
|
data = null;
|
||||||
|
timestamp = 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEmpty() {
|
||||||
|
ensureLoaded();
|
||||||
|
return (skin == null) && (cape == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clear() {
|
||||||
|
profile.removeProperty(PROPERTY_NAME);
|
||||||
|
loaded = false;
|
||||||
|
data = null;
|
||||||
|
timestamp = 0L;
|
||||||
|
skin = null;
|
||||||
|
skinModel = SkinModel.CLASSIC;
|
||||||
|
cape = null;
|
||||||
|
dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URL getSkin() {
|
||||||
|
ensureLoaded();
|
||||||
|
return skin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSkin(URL skinUrl) {
|
||||||
|
setSkin(skinUrl, SkinModel.CLASSIC);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSkin(URL skinUrl, SkinModel skinModel) {
|
||||||
|
validateTextureUrl(skinUrl);
|
||||||
|
if (skinModel == null) skinModel = SkinModel.CLASSIC;
|
||||||
|
// This also loads the textures if necessary:
|
||||||
|
if (Objects.equals(getSkin(), skinUrl) && Objects.equals(getSkinModel(), skinModel)) return;
|
||||||
|
this.skin = skinUrl;
|
||||||
|
this.skinModel = (skinUrl != null) ? skinModel : SkinModel.CLASSIC;
|
||||||
|
markDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SkinModel getSkinModel() {
|
||||||
|
ensureLoaded();
|
||||||
|
return skinModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URL getCape() {
|
||||||
|
ensureLoaded();
|
||||||
|
return cape;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setCape(URL capeUrl) {
|
||||||
|
validateTextureUrl(capeUrl);
|
||||||
|
// This also loads the textures if necessary:
|
||||||
|
if (Objects.equals(getCape(), capeUrl)) return;
|
||||||
|
this.cape = capeUrl;
|
||||||
|
markDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getTimestamp() {
|
||||||
|
ensureLoaded();
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSigned() {
|
||||||
|
if (dirty) return false;
|
||||||
|
Property property = getProperty();
|
||||||
|
return property != null && CraftProfileProperty.hasValidSignature(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
Property getProperty() {
|
||||||
|
rebuildPropertyIfDirty();
|
||||||
|
return profile.getProperty(PROPERTY_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
void rebuildPropertyIfDirty() {
|
||||||
|
if (!dirty) return;
|
||||||
|
// Assert: loaded
|
||||||
|
dirty = false;
|
||||||
|
|
||||||
|
if (isEmpty()) {
|
||||||
|
profile.removeProperty(PROPERTY_NAME);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This produces a new textures property that does not contain any attributes that are specific to official
|
||||||
|
// GameProfiles (such as the property signature, timestamp, profileId, playerName, etc.).
|
||||||
|
// Information on the format of the textures property:
|
||||||
|
// * https://minecraft.fandom.com/wiki/Head#Item_data
|
||||||
|
// * https://wiki.vg/Mojang_API#UUID_to_Profile_and_Skin.2FCape
|
||||||
|
// The order of Json object elements is important.
|
||||||
|
JsonObject propertyData = new JsonObject();
|
||||||
|
|
||||||
|
if (skin != null) {
|
||||||
|
JsonObject texturesMap = JsonHelper.getOrCreateObject(propertyData, "textures");
|
||||||
|
JsonObject skinTexture = JsonHelper.getOrCreateObject(texturesMap, MinecraftProfileTexture.Type.SKIN.name());
|
||||||
|
skinTexture.addProperty("url", skin.toExternalForm());
|
||||||
|
|
||||||
|
// Special case: If the skin model is classic (i.e. default), omit it.
|
||||||
|
// Assert: skinModel != null
|
||||||
|
if (skinModel != SkinModel.CLASSIC) {
|
||||||
|
JsonObject metadata = JsonHelper.getOrCreateObject(skinTexture, "metadata");
|
||||||
|
metadata.addProperty("model", skinModel.name().toLowerCase(Locale.ROOT));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cape != null) {
|
||||||
|
JsonObject texturesMap = JsonHelper.getOrCreateObject(propertyData, "textures");
|
||||||
|
JsonObject skinTexture = JsonHelper.getOrCreateObject(texturesMap, MinecraftProfileTexture.Type.CAPE.name());
|
||||||
|
skinTexture.addProperty("url", cape.toExternalForm());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.data = propertyData;
|
||||||
|
|
||||||
|
// We use the compact formatter here since this is more likely to match the output of existing popular tools
|
||||||
|
// that also create profiles with custom textures:
|
||||||
|
String encodedTexturesData = CraftProfileProperty.encodePropertyValue(propertyData, CraftProfileProperty.JsonFormatter.COMPACT);
|
||||||
|
Property property = new Property(PROPERTY_NAME, encodedTexturesData);
|
||||||
|
profile.setProperty(PROPERTY_NAME, property);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonObject getData() {
|
||||||
|
ensureLoaded();
|
||||||
|
rebuildPropertyIfDirty();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
builder.append("CraftPlayerTextures [data=");
|
||||||
|
builder.append(getData());
|
||||||
|
builder.append("]");
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
Property property = getProperty();
|
||||||
|
return (property == null) ? 0 : CraftProfileProperty.hashCode(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) return true;
|
||||||
|
if (!(obj instanceof CraftPlayerTextures)) return false;
|
||||||
|
CraftPlayerTextures other = (CraftPlayerTextures) obj;
|
||||||
|
Property property = getProperty();
|
||||||
|
Property otherProperty = other.getProperty();
|
||||||
|
return CraftProfileProperty.equals(property, otherProperty);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,139 @@
|
|||||||
|
package org.bukkit.craftbukkit.profile;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParseException;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import com.mojang.authlib.properties.Property;
|
||||||
|
import com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.spec.X509EncodedKeySpec;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import org.apache.commons.io.IOUtils;
|
||||||
|
import org.bukkit.craftbukkit.configuration.ConfigSerializationUtil;
|
||||||
|
|
||||||
|
final class CraftProfileProperty {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Different JSON formatting styles to use for encoded property values.
|
||||||
|
*/
|
||||||
|
public interface JsonFormatter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link JsonFormatter} that uses a compact formatting style.
|
||||||
|
*/
|
||||||
|
public static final JsonFormatter COMPACT = new JsonFormatter() {
|
||||||
|
|
||||||
|
private final Gson gson = new GsonBuilder().create();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String format(JsonElement jsonElement) {
|
||||||
|
return gson.toJson(jsonElement);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public String format(JsonElement jsonElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final PublicKey PUBLIC_KEY;
|
||||||
|
|
||||||
|
static {
|
||||||
|
try {
|
||||||
|
X509EncodedKeySpec spec = new X509EncodedKeySpec(IOUtils.toByteArray(YggdrasilMinecraftSessionService.class.getResourceAsStream("/yggdrasil_session_pubkey.der")));
|
||||||
|
PUBLIC_KEY = KeyFactory.getInstance("RSA").generatePublic(spec);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new Error("Could not find yggdrasil_session_pubkey.der! This indicates a bug.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean hasValidSignature(@Nonnull Property property) {
|
||||||
|
return property.hasSignature() && property.isSignatureValid(PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static String decodeBase64(@Nonnull String encoded) {
|
||||||
|
try {
|
||||||
|
return new String(Base64.getDecoder().decode(encoded), StandardCharsets.UTF_8);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return null; // Invalid input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static JsonObject decodePropertyValue(@Nonnull String encodedPropertyValue) {
|
||||||
|
String json = decodeBase64(encodedPropertyValue);
|
||||||
|
if (json == null) return null;
|
||||||
|
try {
|
||||||
|
JsonElement jsonElement = JsonParser.parseString(json);
|
||||||
|
if (!jsonElement.isJsonObject()) return null;
|
||||||
|
return jsonElement.getAsJsonObject();
|
||||||
|
} catch (JsonParseException e) {
|
||||||
|
return null; // Invalid input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static String encodePropertyValue(@Nonnull JsonObject propertyValue, @Nonnull JsonFormatter formatter) {
|
||||||
|
String json = formatter.format(propertyValue);
|
||||||
|
return Base64.getEncoder().encodeToString(json.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static String toString(@Nonnull Property property) {
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
builder.append("{");
|
||||||
|
builder.append("name=");
|
||||||
|
builder.append(property.getName());
|
||||||
|
builder.append(", value=");
|
||||||
|
builder.append(property.getValue());
|
||||||
|
builder.append(", signature=");
|
||||||
|
builder.append(property.getSignature());
|
||||||
|
builder.append("}");
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int hashCode(@Nonnull Property property) {
|
||||||
|
int result = 1;
|
||||||
|
result = 31 * result + Objects.hashCode(property.getName());
|
||||||
|
result = 31 * result + Objects.hashCode(property.getValue());
|
||||||
|
result = 31 * result + Objects.hashCode(property.getSignature());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean equals(@Nullable Property property, @Nullable Property other) {
|
||||||
|
if (property == null || other == null) return (property == other);
|
||||||
|
if (!Objects.equals(property.getValue(), other.getValue())) return false;
|
||||||
|
if (!Objects.equals(property.getName(), other.getName())) return false;
|
||||||
|
if (!Objects.equals(property.getSignature(), other.getSignature())) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Map<String, Object> serialize(@Nonnull Property property) {
|
||||||
|
Map<String, Object> map = new LinkedHashMap<>();
|
||||||
|
map.put("name", property.getName());
|
||||||
|
map.put("value", property.getValue());
|
||||||
|
if (property.hasSignature()) {
|
||||||
|
map.put("signature", property.getSignature());
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Property deserialize(@Nonnull Map<?, ?> map) {
|
||||||
|
String name = ConfigSerializationUtil.getString(map, "name", false);
|
||||||
|
String value = ConfigSerializationUtil.getString(map, "value", false);
|
||||||
|
String signature = ConfigSerializationUtil.getString(map, "signature", true);
|
||||||
|
return new Property(name, value, signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CraftProfileProperty() {
|
||||||
|
}
|
||||||
|
}
|
49
src/main/java/org/bukkit/craftbukkit/util/JsonHelper.java
Normal file
49
src/main/java/org/bukkit/craftbukkit/util/JsonHelper.java
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package org.bukkit.craftbukkit.util;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonPrimitive;
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public final class JsonHelper {
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static JsonObject getObjectOrNull(@Nonnull JsonObject parent, @Nonnull String key) {
|
||||||
|
JsonElement element = parent.get(key);
|
||||||
|
return (element instanceof JsonObject) ? (JsonObject) element : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static JsonObject getOrCreateObject(@Nonnull JsonObject parent, @Nonnull String key) {
|
||||||
|
JsonObject jsonObject = getObjectOrNull(parent, key);
|
||||||
|
if (jsonObject == null) {
|
||||||
|
jsonObject = new JsonObject();
|
||||||
|
parent.add(key, jsonObject);
|
||||||
|
}
|
||||||
|
return jsonObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static JsonPrimitive getPrimitiveOrNull(@Nonnull JsonObject parent, @Nonnull String key) {
|
||||||
|
JsonElement element = parent.get(key);
|
||||||
|
return (element instanceof JsonPrimitive) ? (JsonPrimitive) element : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static String getStringOrNull(JsonObject parent, String key) {
|
||||||
|
JsonPrimitive primitive = getPrimitiveOrNull(parent, key);
|
||||||
|
return (primitive != null) ? primitive.getAsString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setOrRemove(@Nonnull JsonObject parent, @Nonnull String key, @Nullable JsonElement value) {
|
||||||
|
if (value == null) {
|
||||||
|
parent.remove(key);
|
||||||
|
} else {
|
||||||
|
parent.add(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonHelper() {
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,260 @@
|
|||||||
|
package org.bukkit.craftbukkit.profile;
|
||||||
|
|
||||||
|
import com.mojang.authlib.GameProfile;
|
||||||
|
import com.mojang.authlib.properties.Property;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.bukkit.configuration.InvalidConfigurationException;
|
||||||
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
|
import org.bukkit.configuration.serialization.ConfigurationSerialization;
|
||||||
|
import org.bukkit.profile.PlayerProfile;
|
||||||
|
import org.bukkit.profile.PlayerTextures;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class PlayerProfileTest {
|
||||||
|
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
"timestamp" : 1636282649111,
|
||||||
|
"profileId" : "29a4042b05ab4c7294607aa3b567e8da",
|
||||||
|
"profileName" : "DerFrZocker",
|
||||||
|
"signatureRequired" : true,
|
||||||
|
"textures" : {
|
||||||
|
"SKIN" : {
|
||||||
|
"url" : "http://textures.minecraft.net/texture/284dbf60700b9882c0c2ad1943b515cc111f0b4e562a9a36682495636d846754",
|
||||||
|
"metadata" : {
|
||||||
|
"model" : "slim"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CAPE" : {
|
||||||
|
"url" : "http://textures.minecraft.net/texture/2340c0e03dd24a11b15a8b33c2a7e9e32abb2051b2481d0ba7defd635ca7a933"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
private static final UUID UNIQUE_ID = UUID.fromString("29a4042b-05ab-4c72-9460-7aa3b567e8da");
|
||||||
|
private static final String NAME = "DerFrZocker";
|
||||||
|
private static final URL SKIN;
|
||||||
|
private static final URL CAPE;
|
||||||
|
static {
|
||||||
|
try {
|
||||||
|
SKIN = new URL("http://textures.minecraft.net/texture/284dbf60700b9882c0c2ad1943b515cc111f0b4e562a9a36682495636d846754");
|
||||||
|
CAPE = new URL("http://textures.minecraft.net/texture/2340c0e03dd24a11b15a8b33c2a7e9e32abb2051b2481d0ba7defd635ca7a933");
|
||||||
|
} catch (MalformedURLException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static final long TIMESTAMP = 1636282649111L;
|
||||||
|
private static final String VALUE = "ewogICJ0aW1lc3RhbXAiIDogMTYzNjI4MjY0OTExMSwKICAicHJvZmlsZUlkIiA6ICIyOWE0MDQyYjA1YWI0YzcyOTQ2MDdhYTNiNTY3ZThkYSIsCiAgInByb2ZpbGVOYW1lIiA6ICJEZXJGclpvY2tlciIsCiAgInNpZ25hdHVyZVJlcXVpcmVkIiA6IHRydWUsCiAgInRleHR1cmVzIiA6IHsKICAgICJTS0lOIiA6IHsKICAgICAgInVybCIgOiAiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS8yODRkYmY2MDcwMGI5ODgyYzBjMmFkMTk0M2I1MTVjYzExMWYwYjRlNTYyYTlhMzY2ODI0OTU2MzZkODQ2NzU0IiwKICAgICAgIm1ldGFkYXRhIiA6IHsKICAgICAgICAibW9kZWwiIDogInNsaW0iCiAgICAgIH0KICAgIH0sCiAgICAiQ0FQRSIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMjM0MGMwZTAzZGQyNGExMWIxNWE4YjMzYzJhN2U5ZTMyYWJiMjA1MWIyNDgxZDBiYTdkZWZkNjM1Y2E3YTkzMyIKICAgIH0KICB9Cn0=";
|
||||||
|
private static final String SIGNATURE = "lci91bn2RkJyQ6gGlfTaTJW3afJopeB5Sud2cgVlQJgEvL3j7kIvnCls+2otzVnI6tzbzoXSHbSnfxs7QW+Rv8bGcoH3lAC8UAkwu6ZOLjSxf3e0l4VJJ5lQncI8PG75tGQuQTldnriAhvtV6Q5c0J7aef0bpin+N31NChh/JdZcjpz9zKXkbNph/sZybGY9OlzBcn0Wd8ZVJOKzTLRKtjC8Z7Eu1pd6ZY6WgAoM+nwzag4EAwk+5HhZxSw/r8tentoGK/6/r8oleIDMJVxDPOglnJoFQJMKjC5nrsNBYx59O7I89JDN02jNIdPSdfPwnbgSiaPzIb+o9AA775iDBsF1bPIZ99dc2cXggVA10eQhSaSWRwfDQ0kkiv9YmdKuPpNhewbmTF4bGz0H3v71pOMHT6bvV5qq7IT3XgqK3YwDrIxH2kpE2K6jsbldjDF2uKs0DPDkjPZArT0L/TxwEf02QzLVxU3ctCk6J7VvGQHTqF9vQHnJLWQNjoXG2W4NfPtH2IaYqiecX0PMc6eL+5RtlCs6viRawx8gOjSEKs3MtvV3BqWB3EDFUc1quuLEiDS3R2NSVScOS7CWhiQWCeh2fjm4lnPHA9OmhoMZcnuy0sdPMDu2Omjd8vVZDv/mqlf6Z7O8+mQSockpOFHmaYhTIGO3qRjdMmQdB3YGLVE=";
|
||||||
|
// {"textures":{"SKIN":{"url":"http://textures.minecraft.net/texture/b72144309873464f239d9ae0ec49d2e7f9670552cda8a7a85a76282dd09e14dd"}}}
|
||||||
|
private static final String COMPACT_VALUE = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMjg0ZGJmNjA3MDBiOTg4MmMwYzJhZDE5NDNiNTE1Y2MxMTFmMGI0ZTU2MmE5YTM2NjgyNDk1NjM2ZDg0Njc1NCJ9fX0=";
|
||||||
|
|
||||||
|
private static CraftPlayerProfile buildPlayerProfile() {
|
||||||
|
GameProfile gameProfile = new GameProfile(UNIQUE_ID, NAME);
|
||||||
|
gameProfile.getProperties().put(CraftPlayerTextures.PROPERTY_NAME, new Property(CraftPlayerTextures.PROPERTY_NAME, VALUE, SIGNATURE));
|
||||||
|
return new CraftPlayerProfile(gameProfile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProvidedValues() {
|
||||||
|
Property property = new Property(CraftPlayerTextures.PROPERTY_NAME, VALUE, SIGNATURE);
|
||||||
|
Assert.assertTrue("Invalid test property signature, has the public key changed?", CraftProfileProperty.hasValidSignature(property));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProfileCreation() {
|
||||||
|
// Invalid profiles:
|
||||||
|
Assert.assertThrows(IllegalArgumentException.class, () -> {
|
||||||
|
new CraftPlayerProfile(null, null);
|
||||||
|
});
|
||||||
|
Assert.assertThrows(IllegalArgumentException.class, () -> {
|
||||||
|
new CraftPlayerProfile(null, "");
|
||||||
|
});
|
||||||
|
Assert.assertThrows(IllegalArgumentException.class, () -> {
|
||||||
|
new CraftPlayerProfile(null, " ");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Valid profiles:
|
||||||
|
new CraftPlayerProfile(UNIQUE_ID, null);
|
||||||
|
new CraftPlayerProfile(null, NAME);
|
||||||
|
new CraftPlayerProfile(UNIQUE_ID, NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGameProfileWrapping() {
|
||||||
|
// Invalid profiles:
|
||||||
|
Assert.assertThrows(NullPointerException.class, () -> {
|
||||||
|
new CraftPlayerProfile(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Valid profiles:
|
||||||
|
CraftPlayerProfile profile1 = new CraftPlayerProfile(new GameProfile(UNIQUE_ID, NAME));
|
||||||
|
Assert.assertEquals("Unique id is not the same", UNIQUE_ID, profile1.getUniqueId());
|
||||||
|
Assert.assertEquals("Name is not the same", NAME, profile1.getName());
|
||||||
|
|
||||||
|
CraftPlayerProfile profile2 = new CraftPlayerProfile(new GameProfile(UNIQUE_ID, null));
|
||||||
|
Assert.assertEquals("Unique id is not the same", UNIQUE_ID, profile2.getUniqueId());
|
||||||
|
Assert.assertEquals("Name is not null", null, profile2.getName());
|
||||||
|
|
||||||
|
CraftPlayerProfile profile3 = new CraftPlayerProfile(new GameProfile(null, NAME));
|
||||||
|
Assert.assertEquals("Unique id is not null", null, profile3.getUniqueId());
|
||||||
|
Assert.assertEquals("Name is not the same", NAME, profile3.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTexturesLoading() {
|
||||||
|
CraftPlayerProfile profile = buildPlayerProfile();
|
||||||
|
Assert.assertEquals("Unique id is not the same", UNIQUE_ID, profile.getUniqueId());
|
||||||
|
Assert.assertEquals("Name is not the same", NAME, profile.getName());
|
||||||
|
Assert.assertEquals("Skin url is not the same", SKIN, profile.getTextures().getSkin());
|
||||||
|
Assert.assertEquals("Skin model is not the same", PlayerTextures.SkinModel.SLIM, profile.getTextures().getSkinModel());
|
||||||
|
Assert.assertEquals("Cape url is not the same", CAPE, profile.getTextures().getCape());
|
||||||
|
Assert.assertEquals("Timestamp is not the same", TIMESTAMP, profile.getTextures().getTimestamp());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBuildGameProfile() {
|
||||||
|
CraftPlayerProfile profile = buildPlayerProfile();
|
||||||
|
GameProfile gameProfile = profile.buildGameProfile();
|
||||||
|
Assert.assertNotNull("GameProfile is null", gameProfile);
|
||||||
|
|
||||||
|
Property property = CraftPlayerProfile.getProperty(gameProfile, CraftPlayerTextures.PROPERTY_NAME);
|
||||||
|
Assert.assertNotNull("Textures property is null", property);
|
||||||
|
Assert.assertEquals("Property values are not the same", VALUE, property.getValue());
|
||||||
|
Assert.assertEquals("Names are not the same", NAME, gameProfile.getName());
|
||||||
|
Assert.assertEquals("Unique ids are not the same", UNIQUE_ID, gameProfile.getId());
|
||||||
|
Assert.assertTrue("Signature is missing", property.hasSignature());
|
||||||
|
Assert.assertTrue("Signature is not valid", CraftProfileProperty.hasValidSignature(property));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBuildGameProfileReturnsNewInstance() {
|
||||||
|
CraftPlayerProfile profile = buildPlayerProfile();
|
||||||
|
GameProfile gameProfile1 = profile.buildGameProfile();
|
||||||
|
GameProfile gameProfile2 = profile.buildGameProfile();
|
||||||
|
Assert.assertTrue("CraftPlayerProfile#buildGameProfile() does not produce a new instance", gameProfile1 != gameProfile2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSignatureValidation() {
|
||||||
|
CraftPlayerProfile profile = buildPlayerProfile();
|
||||||
|
Assert.assertTrue("Signature is not valid", profile.getTextures().isSigned());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSignatureInvalidation() {
|
||||||
|
CraftPlayerProfile profile = buildPlayerProfile();
|
||||||
|
profile.getTextures().setSkin(null);
|
||||||
|
Assert.assertTrue("Textures has a timestamp", profile.getTextures().getTimestamp() == 0L);
|
||||||
|
Assert.assertTrue("Textures signature is valid", !profile.getTextures().isSigned());
|
||||||
|
|
||||||
|
// Ensure that the invalidation is preserved when the property is rebuilt:
|
||||||
|
profile.rebuildDirtyProperties();
|
||||||
|
Assert.assertTrue("Rebuilt textures has a timestamp", profile.getTextures().getTimestamp() == 0L);
|
||||||
|
Assert.assertTrue("Rebuilt textures signature is valid", !profile.getTextures().isSigned());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetSkinResetsSkinModel() {
|
||||||
|
CraftPlayerProfile profile = buildPlayerProfile();
|
||||||
|
Assert.assertEquals("Skin model is not the same", PlayerTextures.SkinModel.SLIM, profile.getTextures().getSkinModel());
|
||||||
|
profile.getTextures().setSkin(SKIN);
|
||||||
|
Assert.assertEquals("Skin model was not reset by skin change", PlayerTextures.SkinModel.CLASSIC, profile.getTextures().getSkinModel());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetTextures() {
|
||||||
|
CraftPlayerProfile profile = buildPlayerProfile();
|
||||||
|
CraftPlayerProfile profile2 = new CraftPlayerProfile(new GameProfile(UNIQUE_ID, NAME));
|
||||||
|
|
||||||
|
Assert.assertTrue("profile has no textures", !profile.getTextures().isEmpty());
|
||||||
|
Assert.assertTrue("profile2 has textures", profile2.getTextures().isEmpty());
|
||||||
|
|
||||||
|
profile2.setTextures(profile.getTextures());
|
||||||
|
Assert.assertTrue("profile2 has no textures", !profile2.getTextures().isEmpty());
|
||||||
|
Assert.assertEquals("copied profile textures are not the same", profile.getTextures(), profile2.getTextures());
|
||||||
|
|
||||||
|
profile2.setTextures(null);
|
||||||
|
Assert.assertTrue("cleared profile2 has textures", profile2.getTextures().isEmpty());
|
||||||
|
Assert.assertTrue("cleared profile2 has textures timestamp", profile2.getTextures().getTimestamp() == 0L);
|
||||||
|
Assert.assertTrue("cleared profile2 has signed textures", !profile2.getTextures().isSigned());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testClearTextures() {
|
||||||
|
CraftPlayerProfile profile = buildPlayerProfile();
|
||||||
|
Assert.assertTrue("profile has no textures", !profile.getTextures().isEmpty());
|
||||||
|
|
||||||
|
profile.getTextures().clear();
|
||||||
|
Assert.assertTrue("cleared profile has textures", profile.getTextures().isEmpty());
|
||||||
|
Assert.assertTrue("cleared profile has textures timestamp", profile.getTextures().getTimestamp() == 0L);
|
||||||
|
Assert.assertTrue("cleared profile has signed textures", !profile.getTextures().isSigned());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCustomSkin() {
|
||||||
|
CraftPlayerProfile profile = new CraftPlayerProfile(UNIQUE_ID, NAME);
|
||||||
|
profile.getTextures().setSkin(SKIN);
|
||||||
|
Assert.assertEquals("profile with custom skin does not match expected value", COMPACT_VALUE, profile.getTextures().getProperty().getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEquals() {
|
||||||
|
CraftPlayerProfile profile1 = buildPlayerProfile();
|
||||||
|
CraftPlayerProfile profile2 = buildPlayerProfile();
|
||||||
|
CraftPlayerProfile profile3 = new CraftPlayerProfile(new GameProfile(UNIQUE_ID, NAME));
|
||||||
|
CraftPlayerProfile profile4 = new CraftPlayerProfile(new GameProfile(UNIQUE_ID, NAME));
|
||||||
|
CraftPlayerProfile profile5 = new CraftPlayerProfile(new GameProfile(UNIQUE_ID, null));
|
||||||
|
CraftPlayerProfile profile6 = new CraftPlayerProfile(new GameProfile(null, NAME));
|
||||||
|
|
||||||
|
Assert.assertEquals("profile1 and profile2 are not equal", profile1, profile2);
|
||||||
|
Assert.assertEquals("profile3 and profile4 are not equal", profile3, profile4);
|
||||||
|
Assert.assertNotEquals("profile1 and profile3 are equal", profile1, profile3);
|
||||||
|
Assert.assertNotEquals("profile4 and profile5 are equal", profile4, profile5);
|
||||||
|
Assert.assertNotEquals("profile4 and profile6 are equal", profile4, profile6);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTexturesEquals() {
|
||||||
|
CraftPlayerProfile profile1 = buildPlayerProfile();
|
||||||
|
CraftPlayerProfile profile2 = buildPlayerProfile();
|
||||||
|
Assert.assertEquals("Profile textures are not equal", profile1.getTextures(), profile2.getTextures());
|
||||||
|
|
||||||
|
profile1.getTextures().setCape(null);
|
||||||
|
Assert.assertNotEquals("Modified profile textures are still equal", profile1.getTextures(), profile2.getTextures());
|
||||||
|
|
||||||
|
profile2.getTextures().setCape(null);
|
||||||
|
Assert.assertEquals("Modified profile textures are not equal", profile1.getTextures(), profile2.getTextures());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testClone() {
|
||||||
|
PlayerProfile profile = buildPlayerProfile();
|
||||||
|
PlayerProfile copy = profile.clone();
|
||||||
|
Assert.assertEquals("profile and copy are not equal", profile, copy);
|
||||||
|
|
||||||
|
// New copies are independent (don't affect the original profile):
|
||||||
|
copy.getTextures().setSkin(null);
|
||||||
|
Assert.assertEquals("copy is not independent", SKIN, profile.getTextures().getSkin());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSerializationFullProfile() throws InvalidConfigurationException {
|
||||||
|
ConfigurationSerialization.registerClass(CraftPlayerProfile.class);
|
||||||
|
PlayerProfile playerProfile = buildPlayerProfile();
|
||||||
|
YamlConfiguration configuration = new YamlConfiguration();
|
||||||
|
|
||||||
|
configuration.set("test", playerProfile);
|
||||||
|
|
||||||
|
String saved = configuration.saveToString();
|
||||||
|
|
||||||
|
configuration = new YamlConfiguration();
|
||||||
|
configuration.loadFromString(saved);
|
||||||
|
|
||||||
|
Assert.assertTrue(configuration.contains("test"));
|
||||||
|
Assert.assertEquals("Profiles are not equal", playerProfile, configuration.get("test"));
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user