From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Aikar <aikar@aikar.co>
Date: Mon, 15 Jan 2018 22:11:48 -0500
Subject: [PATCH] Basic PlayerProfile API

Establishes base extension of profile systems for future edits too

diff --git a/src/main/java/com/destroystokyo/paper/profile/CraftPlayerProfile.java b/src/main/java/com/destroystokyo/paper/profile/CraftPlayerProfile.java
new file mode 100644
index 0000000000000000000000000000000000000000..3ff790cec1ad89caec4be64421dd7d51652be598
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/profile/CraftPlayerProfile.java
@@ -0,0 +1,399 @@
+package com.destroystokyo.paper.profile;
+
+import io.papermc.paper.configuration.GlobalConfiguration;
+import com.google.common.base.Charsets;
+import com.google.common.collect.Iterables;
+import com.mojang.authlib.GameProfile;
+import com.mojang.authlib.properties.Property;
+import com.mojang.authlib.properties.PropertyMap;
+import net.minecraft.Util;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.players.GameProfileCache;
+import org.apache.commons.lang3.Validate;
+import org.bukkit.configuration.serialization.SerializableAs;
+import org.bukkit.craftbukkit.configuration.ConfigSerializationUtil;
+import org.bukkit.craftbukkit.entity.CraftPlayer;
+import org.bukkit.craftbukkit.profile.CraftPlayerTextures;
+import org.bukkit.craftbukkit.profile.CraftProfileProperty;
+import org.bukkit.profile.PlayerTextures;
+import org.jetbrains.annotations.NotNull;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+
+@SerializableAs("PlayerProfile")
+public class CraftPlayerProfile implements PlayerProfile, SharedPlayerProfile {
+
+    private GameProfile profile;
+    private final PropertySet properties = new PropertySet();
+
+    public CraftPlayerProfile(CraftPlayer player) {
+        this.profile = player.getHandle().getGameProfile();
+    }
+
+    public CraftPlayerProfile(UUID id, String name) {
+        this.profile = new GameProfile(id, name);
+    }
+
+    public CraftPlayerProfile(GameProfile profile) {
+        Validate.notNull(profile, "GameProfile cannot be null!");
+        this.profile = profile;
+    }
+
+    @Override
+    public boolean hasProperty(String property) {
+        return profile.getProperties().containsKey(property);
+    }
+
+    @Override
+    public void setProperty(ProfileProperty property) {
+        String name = property.getName();
+        PropertyMap properties = profile.getProperties();
+        properties.removeAll(name);
+        properties.put(name, new Property(name, property.getValue(), property.getSignature()));
+    }
+
+    @Override
+    public CraftPlayerTextures getTextures() {
+        return new CraftPlayerTextures(this);
+    }
+
+    @Override
+    public void setTextures(@Nullable PlayerTextures textures) {
+        if (textures == null) {
+            this.removeProperty("textures");
+        } else {
+            CraftPlayerTextures craftPlayerTextures = new CraftPlayerTextures(this);
+            craftPlayerTextures.copyFrom(textures);
+            craftPlayerTextures.rebuildPropertyIfDirty();
+        }
+    }
+
+    public GameProfile getGameProfile() {
+        return profile;
+    }
+
+    @Nullable
+    @Override
+    public UUID getId() {
+        return profile.getId();
+    }
+
+    @Override
+    @Deprecated(forRemoval = true)
+    public UUID setId(@Nullable UUID uuid) {
+        GameProfile prev = this.profile;
+        this.profile = new GameProfile(uuid, prev.getName());
+        copyProfileProperties(prev, this.profile);
+        return prev.getId();
+    }
+
+    @Override
+    public UUID getUniqueId() {
+        return getId();
+    }
+
+    @Nullable
+    @Override
+    public String getName() {
+        return profile.getName();
+    }
+
+    @Override
+    @Deprecated(forRemoval = true)
+    public String setName(@Nullable String name) {
+        GameProfile prev = this.profile;
+        this.profile = new GameProfile(prev.getId(), name);
+        copyProfileProperties(prev, this.profile);
+        return prev.getName();
+    }
+
+    @Nonnull
+    @Override
+    public Set<ProfileProperty> getProperties() {
+        return properties;
+    }
+
+    @Override
+    public void setProperties(Collection<ProfileProperty> properties) {
+        properties.forEach(this::setProperty);
+    }
+
+    @Override
+    public void clearProperties() {
+        profile.getProperties().clear();
+    }
+
+    @Override
+    public boolean removeProperty(String property) {
+        return !profile.getProperties().removeAll(property).isEmpty();
+    }
+
+    @Nullable
+    @Override
+    public Property getProperty(String property) {
+        return Iterables.getFirst(this.profile.getProperties().get(property), null);
+    }
+
+    @Nullable
+    @Override
+    public void setProperty(@NotNull String propertyName, @Nullable Property property) {
+        PropertyMap properties = profile.getProperties();
+        properties.removeAll(propertyName);
+        if (property != null) {
+            properties.put(propertyName, property);
+        }
+    }
+
+    @Override
+    public @NotNull GameProfile buildGameProfile() {
+        GameProfile profile = new GameProfile(this.profile.getId(), this.profile.getName());
+        profile.getProperties().putAll(this.profile.getProperties());
+        return profile;
+    }
+
+    @Override
+    public CraftPlayerProfile clone() {
+        CraftPlayerProfile clone = new CraftPlayerProfile(this.getId(), this.getName());
+        clone.setProperties(getProperties());
+        return clone;
+    }
+
+    @Override
+    public boolean isComplete() {
+        return profile.isComplete();
+    }
+
+    @Override
+    public @NotNull CompletableFuture<PlayerProfile> update() {
+        return CompletableFuture.supplyAsync(() -> {
+            final CraftPlayerProfile clone = clone();
+            clone.complete(true);
+            return clone;
+        }, Util.PROFILE_EXECUTOR);
+    }
+
+    @Override
+    public boolean completeFromCache() {
+        return completeFromCache(false, GlobalConfiguration.get().proxies.isProxyOnlineMode());
+    }
+
+    public boolean completeFromCache(boolean onlineMode) {
+        return completeFromCache(false, onlineMode);
+    }
+
+    public boolean completeFromCache(boolean lookupUUID, boolean onlineMode) {
+        MinecraftServer server = MinecraftServer.getServer();
+        String name = profile.getName();
+        GameProfileCache userCache = server.getProfileCache();
+        if (profile.getId() == null) {
+            final GameProfile profile;
+            if (onlineMode) {
+                profile = lookupUUID ? userCache.get(name).orElse(null) : userCache.getProfileIfCached(name);
+            } else {
+                // Make an OfflinePlayer using an offline mode UUID since the name has no profile
+                profile = new GameProfile(UUID.nameUUIDFromBytes(("OfflinePlayer:" + name).getBytes(Charsets.UTF_8)), name);
+            }
+            if (profile != null) {
+                // if old has it, assume its newer, so overwrite, else use cached if it was set and ours wasn't
+                copyProfileProperties(this.profile, profile);
+                this.profile = profile;
+            }
+        }
+
+        if ((profile.getName() == null || !hasTextures()) && profile.getId() != null) {
+            Optional<GameProfile> optProfile = userCache.get(this.profile.getId());
+            if (optProfile.isPresent()) {
+                GameProfile profile = optProfile.get();
+                if (this.profile.getName() == null) {
+                    // if old has it, assume its newer, so overwrite, else use cached if it was set and ours wasn't
+                    copyProfileProperties(this.profile, profile);
+                    this.profile = profile;
+                } else {
+                    copyProfileProperties(profile, this.profile);
+                }
+            }
+        }
+        return this.profile.isComplete();
+    }
+
+    public boolean complete(boolean textures) {
+        return complete(textures, GlobalConfiguration.get().proxies.isProxyOnlineMode());
+    }
+    public boolean complete(boolean textures, boolean onlineMode) {
+        MinecraftServer server = MinecraftServer.getServer();
+        boolean isCompleteFromCache = this.completeFromCache(true, onlineMode);
+        if (onlineMode && (!isCompleteFromCache || textures && !hasTextures())) {
+            GameProfile result = server.getSessionService().fillProfileProperties(profile, true);
+            if (result != null) {
+                copyProfileProperties(result, this.profile, true);
+            }
+            if (this.profile.isComplete()) {
+                server.getProfileCache().add(this.profile);
+            }
+        }
+        return profile.isComplete() && (!onlineMode || !textures || hasTextures());
+    }
+
+    private static void copyProfileProperties(GameProfile source, GameProfile target) {
+        copyProfileProperties(source, target, false);
+    }
+
+    private static void copyProfileProperties(GameProfile source, GameProfile target, boolean clearTarget) {
+        PropertyMap sourceProperties = source.getProperties();
+        PropertyMap targetProperties = target.getProperties();
+        if (clearTarget) targetProperties.clear();
+        if (sourceProperties.isEmpty()) {
+            return;
+        }
+
+        for (Property property : sourceProperties.values()) {
+            targetProperties.removeAll(property.getName());
+            targetProperties.put(property.getName(), property);
+        }
+    }
+
+    private static ProfileProperty toBukkit(Property property) {
+        return new ProfileProperty(property.getName(), property.getValue(), property.getSignature());
+    }
+
+    public static PlayerProfile asBukkitCopy(GameProfile gameProfile) {
+        CraftPlayerProfile profile = new CraftPlayerProfile(gameProfile.getId(), gameProfile.getName());
+        copyProfileProperties(gameProfile, profile.profile);
+        return profile;
+    }
+
+    public static PlayerProfile asBukkitMirror(GameProfile profile) {
+        return new CraftPlayerProfile(profile);
+    }
+
+    public static Property asAuthlib(ProfileProperty property) {
+        return new Property(property.getName(), property.getValue(), property.getSignature());
+    }
+
+    public static GameProfile asAuthlibCopy(PlayerProfile profile) {
+        CraftPlayerProfile craft = ((CraftPlayerProfile) profile);
+        return asAuthlib(craft.clone());
+    }
+
+    public static GameProfile asAuthlib(PlayerProfile profile) {
+        CraftPlayerProfile craft = ((CraftPlayerProfile) profile);
+        return craft.getGameProfile();
+    }
+
+    @Override
+    public @NotNull Map<String, Object> serialize() {
+        Map<String, Object> map = new LinkedHashMap<>();
+        if (this.getId() != null) {
+            map.put("uniqueId", this.getId().toString());
+        }
+        if (this.getName() != null) {
+            map.put("name", getName());
+        }
+        if (!this.properties.isEmpty()) {
+            List<Object> propertiesData = new ArrayList<>();
+            for (ProfileProperty property : properties) {
+                propertiesData.add(CraftProfileProperty.serialize(new Property(property.getName(), property.getValue(), property.getSignature())));
+            }
+            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.profile.getProperties().put(property.getName(), property);
+            }
+        }
+
+        return profile;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+        if (!(obj instanceof CraftPlayerProfile otherProfile)) return false;
+        return Objects.equals(this.profile, otherProfile.profile);
+    }
+
+    @Override
+    public String toString() {
+        return "CraftPlayerProfile [uniqueId=" + getId() +
+            ", name=" + getName() +
+            ", properties=" + org.bukkit.craftbukkit.profile.CraftPlayerProfile.toString(this.profile.getProperties()) +
+            "]";
+    }
+
+    @Override
+    public int hashCode() {
+        return this.profile.hashCode();
+    }
+
+    private class PropertySet extends AbstractSet<ProfileProperty> {
+
+        @Override
+        @Nonnull
+        public Iterator<ProfileProperty> iterator() {
+            return new ProfilePropertyIterator(profile.getProperties().values().iterator());
+        }
+
+        @Override
+        public int size() {
+            return profile.getProperties().size();
+        }
+
+        @Override
+        public boolean add(ProfileProperty property) {
+            setProperty(property);
+            return true;
+        }
+
+        @Override
+        public boolean addAll(Collection<? extends ProfileProperty> c) {
+            //noinspection unchecked
+            setProperties((Collection<ProfileProperty>) c);
+            return true;
+        }
+
+        @Override
+        public boolean contains(Object o) {
+            return o instanceof ProfileProperty && profile.getProperties().containsKey(((ProfileProperty) o).getName());
+        }
+
+        private class ProfilePropertyIterator implements Iterator<ProfileProperty> {
+            private final Iterator<Property> iterator;
+
+            ProfilePropertyIterator(Iterator<Property> iterator) {
+                this.iterator = iterator;
+            }
+
+            @Override
+            public boolean hasNext() {
+                return iterator.hasNext();
+            }
+
+            @Override
+            public ProfileProperty next() {
+                return toBukkit(iterator.next());
+            }
+
+            @Override
+            public void remove() {
+                iterator.remove();
+            }
+        }
+    }
+}
diff --git a/src/main/java/com/destroystokyo/paper/profile/PaperAuthenticationService.java b/src/main/java/com/destroystokyo/paper/profile/PaperAuthenticationService.java
new file mode 100644
index 0000000000000000000000000000000000000000..1459a1f99fe614d072a087cda18788cf13102645
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/profile/PaperAuthenticationService.java
@@ -0,0 +1,31 @@
+package com.destroystokyo.paper.profile;
+
+import com.mojang.authlib.*;
+import com.mojang.authlib.minecraft.MinecraftSessionService;
+import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService;
+import com.mojang.authlib.yggdrasil.YggdrasilEnvironment;
+
+import java.net.Proxy;
+
+public class PaperAuthenticationService extends YggdrasilAuthenticationService {
+    private final Environment environment;
+    public PaperAuthenticationService(Proxy proxy) {
+        super(proxy);
+        this.environment = EnvironmentParser.getEnvironmentFromProperties().orElse(YggdrasilEnvironment.PROD.getEnvironment());
+    }
+
+    @Override
+    public UserAuthentication createUserAuthentication(Agent agent) {
+        return new PaperUserAuthentication(this, agent);
+    }
+
+    @Override
+    public MinecraftSessionService createMinecraftSessionService() {
+        return new PaperMinecraftSessionService(this, this.environment);
+    }
+
+    @Override
+    public GameProfileRepository createProfileRepository() {
+        return new PaperGameProfileRepository(this, this.environment);
+    }
+}
diff --git a/src/main/java/com/destroystokyo/paper/profile/PaperGameProfileRepository.java b/src/main/java/com/destroystokyo/paper/profile/PaperGameProfileRepository.java
new file mode 100644
index 0000000000000000000000000000000000000000..582c169c85ac66f1f9430f79042e4655f776c157
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/profile/PaperGameProfileRepository.java
@@ -0,0 +1,18 @@
+package com.destroystokyo.paper.profile;
+
+import com.mojang.authlib.Agent;
+import com.mojang.authlib.Environment;
+import com.mojang.authlib.ProfileLookupCallback;
+import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService;
+import com.mojang.authlib.yggdrasil.YggdrasilGameProfileRepository;
+
+public class PaperGameProfileRepository extends YggdrasilGameProfileRepository {
+    public PaperGameProfileRepository(YggdrasilAuthenticationService authenticationService, Environment environment) {
+        super(authenticationService, environment);
+    }
+
+    @Override
+    public void findProfilesByNames(String[] names, Agent agent, ProfileLookupCallback callback) {
+        super.findProfilesByNames(names, agent, callback);
+    }
+}
diff --git a/src/main/java/com/destroystokyo/paper/profile/PaperMinecraftSessionService.java b/src/main/java/com/destroystokyo/paper/profile/PaperMinecraftSessionService.java
new file mode 100644
index 0000000000000000000000000000000000000000..93d73c27340645c7502acafdc0b2cfbc1a759dd8
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/profile/PaperMinecraftSessionService.java
@@ -0,0 +1,30 @@
+package com.destroystokyo.paper.profile;
+
+import com.mojang.authlib.Environment;
+import com.mojang.authlib.GameProfile;
+import com.mojang.authlib.minecraft.MinecraftProfileTexture;
+import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService;
+import com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService;
+
+import java.util.Map;
+
+public class PaperMinecraftSessionService extends YggdrasilMinecraftSessionService {
+    protected PaperMinecraftSessionService(YggdrasilAuthenticationService authenticationService, Environment environment) {
+        super(authenticationService, environment);
+    }
+
+    @Override
+    public Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> getTextures(GameProfile profile, boolean requireSecure) {
+        return super.getTextures(profile, requireSecure);
+    }
+
+    @Override
+    public GameProfile fillProfileProperties(GameProfile profile, boolean requireSecure) {
+        return super.fillProfileProperties(profile, requireSecure);
+    }
+
+    @Override
+    protected GameProfile fillGameProfile(GameProfile profile, boolean requireSecure) {
+        return super.fillGameProfile(profile, requireSecure);
+    }
+}
diff --git a/src/main/java/com/destroystokyo/paper/profile/PaperUserAuthentication.java b/src/main/java/com/destroystokyo/paper/profile/PaperUserAuthentication.java
new file mode 100644
index 0000000000000000000000000000000000000000..3cdd06d3af7ff94f1fe1a11b9a9275e17c695a38
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/profile/PaperUserAuthentication.java
@@ -0,0 +1,12 @@
+package com.destroystokyo.paper.profile;
+
+import com.mojang.authlib.Agent;
+import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService;
+import com.mojang.authlib.yggdrasil.YggdrasilUserAuthentication;
+import java.util.UUID;
+
+public class PaperUserAuthentication extends YggdrasilUserAuthentication {
+    public PaperUserAuthentication(YggdrasilAuthenticationService authenticationService, Agent agent) {
+        super(authenticationService, UUID.randomUUID().toString(), agent);
+    }
+}
diff --git a/src/main/java/com/destroystokyo/paper/profile/SharedPlayerProfile.java b/src/main/java/com/destroystokyo/paper/profile/SharedPlayerProfile.java
new file mode 100644
index 0000000000000000000000000000000000000000..7ac27392a8647ef7d0dc78efe78703e993885017
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/profile/SharedPlayerProfile.java
@@ -0,0 +1,23 @@
+package com.destroystokyo.paper.profile;
+
+import com.mojang.authlib.GameProfile;
+import com.mojang.authlib.properties.Property;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+public interface SharedPlayerProfile {
+
+    @Nullable UUID getUniqueId();
+
+    @Nullable String getName();
+
+    boolean removeProperty(@NotNull String property);
+
+    @Nullable Property getProperty(@NotNull String propertyName);
+
+    @Nullable void setProperty(@NotNull String propertyName, @Nullable Property property);
+
+    @NotNull GameProfile buildGameProfile();
+}
diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java
index c9e169a6dca4dd8fe6e27a23deb410664a5d4466..e98276943e1690572b8f7bc54a259aa8340bae41 100644
--- a/src/main/java/net/minecraft/server/MCUtil.java
+++ b/src/main/java/net/minecraft/server/MCUtil.java
@@ -1,5 +1,7 @@
 package net.minecraft.server;
 
+import com.destroystokyo.paper.profile.CraftPlayerProfile;
+import com.destroystokyo.paper.profile.PlayerProfile;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import com.google.gson.JsonArray;
 import com.google.gson.JsonObject;
@@ -24,6 +26,7 @@ import net.minecraft.world.level.Level;
 import net.minecraft.world.level.chunk.ChunkAccess;
 import net.minecraft.world.level.chunk.ChunkStatus;
 import org.apache.commons.lang.exception.ExceptionUtils;
+import com.mojang.authlib.GameProfile;
 import org.bukkit.Location;
 import org.bukkit.block.BlockFace;
 import org.bukkit.craftbukkit.CraftWorld;
@@ -371,6 +374,10 @@ public final class MCUtil {
         return run.get();
     }
 
+    public static PlayerProfile toBukkit(GameProfile profile) {
+        return CraftPlayerProfile.asBukkitMirror(profile);
+    }
+
     /**
      * Calculates distance between 2 entities
      * @param e1
diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java
index 45db9f1b1d19319e7f92bd4e61be9ea9b06dd5e5..151b13e257c09fc5c4bbccfc388b15ad76133909 100644
--- a/src/main/java/net/minecraft/server/Main.java
+++ b/src/main/java/net/minecraft/server/Main.java
@@ -155,7 +155,7 @@ public class Main {
             }
 
             File file = (File) optionset.valueOf("universe"); // CraftBukkit
-            Services services = Services.create(new YggdrasilAuthenticationService(Proxy.NO_PROXY), file, optionset); // Paper
+            Services services = Services.create(new com.destroystokyo.paper.profile.PaperAuthenticationService(Proxy.NO_PROXY), file, optionset); // Paper
             // CraftBukkit start
             String s = (String) Optional.ofNullable((String) optionset.valueOf("world")).orElse(dedicatedserversettings.getProperties().levelName);
             LevelStorageSource convertable = LevelStorageSource.createDefault(file.toPath());
diff --git a/src/main/java/net/minecraft/server/players/GameProfileCache.java b/src/main/java/net/minecraft/server/players/GameProfileCache.java
index 2a0cf0a8a79c09566c598197fc6f8c447d4bbd72..5e3bc0590e59770490b1c6c818d99be054214a8a 100644
--- a/src/main/java/net/minecraft/server/players/GameProfileCache.java
+++ b/src/main/java/net/minecraft/server/players/GameProfileCache.java
@@ -136,6 +136,17 @@ public class GameProfileCache {
         return this.operationCount.incrementAndGet();
     }
 
+    // Paper start
+    public @Nullable GameProfile getProfileIfCached(String name) {
+        GameProfileCache.GameProfileInfo entry = this.profilesByName.get(name.toLowerCase(Locale.ROOT));
+        if (entry == null) {
+            return null;
+        }
+        entry.setLastAccess(this.getNextOperation());
+        return entry.getProfile();
+    }
+    // Paper end
+
     public Optional<GameProfile> get(String name) {
         String s1 = name.toLowerCase(Locale.ROOT);
         GameProfileCache.GameProfileInfo usercache_usercacheentry = (GameProfileCache.GameProfileInfo) this.profilesByName.get(s1);
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
index 75b9d496860d7f3a9d5f680cbdfe89860286522e..7013b11fc0cd5fbb5a7e62be45d84d54d3052237 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
@@ -244,6 +244,9 @@ import org.yaml.snakeyaml.error.MarkedYAMLException;
 
 import net.md_5.bungee.api.chat.BaseComponent; // Spigot
 
+import javax.annotation.Nullable; // Paper
+import javax.annotation.Nonnull; // Paper
+
 public final class CraftServer implements Server {
     private final String serverName = "Paper"; // Paper
     private final String serverVersion;
@@ -284,6 +287,7 @@ public final class CraftServer implements Server {
     static {
         ConfigurationSerialization.registerClass(CraftOfflinePlayer.class);
         ConfigurationSerialization.registerClass(CraftPlayerProfile.class);
+        ConfigurationSerialization.registerClass(com.destroystokyo.paper.profile.CraftPlayerProfile.class); // Paper
         CraftItemFactory.instance();
     }
 
@@ -2596,5 +2600,37 @@ public final class CraftServer implements Server {
     public boolean suggestPlayerNamesWhenNullTabCompletions() {
         return io.papermc.paper.configuration.GlobalConfiguration.get().commands.suggestPlayerNamesWhenNullTabCompletions;
     }
+
+    @Override
+    public com.destroystokyo.paper.profile.PlayerProfile createProfile(@Nonnull UUID uuid) {
+        return createProfile(uuid, null);
+    }
+
+    @Override
+    public com.destroystokyo.paper.profile.PlayerProfile createProfile(@Nonnull String name) {
+        return createProfile(null, name);
+    }
+
+    @Override
+    public com.destroystokyo.paper.profile.PlayerProfile createProfile(@Nullable UUID uuid, @Nullable String name) {
+        Player player = uuid != null ? Bukkit.getPlayer(uuid) : (name != null ? Bukkit.getPlayerExact(name) : null);
+        if (player != null) return new com.destroystokyo.paper.profile.CraftPlayerProfile((CraftPlayer) player);
+
+        return new com.destroystokyo.paper.profile.CraftPlayerProfile(uuid, name);
+    }
+
+    @Override
+    public com.destroystokyo.paper.profile.PlayerProfile createProfileExact(@Nullable UUID uuid, @Nullable String name) {
+        Player player = uuid != null ? Bukkit.getPlayer(uuid) : (name != null ? Bukkit.getPlayerExact(name) : null);
+        if (player == null) return new com.destroystokyo.paper.profile.CraftPlayerProfile(uuid, name);
+
+        if (Objects.equals(uuid, player.getUniqueId()) && Objects.equals(name, player.getName())) {
+            return new com.destroystokyo.paper.profile.CraftPlayerProfile((CraftPlayer) player);
+        }
+
+        final com.mojang.authlib.GameProfile profile = new com.mojang.authlib.GameProfile(uuid, name);
+        profile.getProperties().putAll(((CraftPlayer)player).getHandle().getGameProfile().getProperties());
+        return new com.destroystokyo.paper.profile.CraftPlayerProfile(profile);
+    }
     // Paper end
 }
diff --git a/src/main/java/org/bukkit/craftbukkit/profile/CraftPlayerProfile.java b/src/main/java/org/bukkit/craftbukkit/profile/CraftPlayerProfile.java
index 9edc5e73819e0b55372f77c5e292eece74d837c7..9001c9dc68dc05944eb839c3354bea29249daa92 100644
--- a/src/main/java/org/bukkit/craftbukkit/profile/CraftPlayerProfile.java
+++ b/src/main/java/org/bukkit/craftbukkit/profile/CraftPlayerProfile.java
@@ -27,7 +27,7 @@ import org.bukkit.profile.PlayerProfile;
 import org.bukkit.profile.PlayerTextures;
 
 @SerializableAs("PlayerProfile")
-public final class CraftPlayerProfile implements PlayerProfile {
+public final class CraftPlayerProfile implements PlayerProfile, com.destroystokyo.paper.profile.SharedPlayerProfile { // Paper
 
     @Nonnull
     public static GameProfile validateSkullProfile(@Nonnull GameProfile gameProfile) {
@@ -92,8 +92,10 @@ public final class CraftPlayerProfile implements PlayerProfile {
         }
     }
 
-    void removeProperty(String propertyName) {
-        this.properties.removeAll(propertyName);
+    // Paper start - change return value for shared interface
+    public boolean removeProperty(String propertyName) {
+        return !this.properties.removeAll(propertyName).isEmpty();
+        // Paper end
     }
 
     void rebuildDirtyProperties() {
@@ -236,6 +238,7 @@ public final class CraftPlayerProfile implements PlayerProfile {
 
     @Override
     public Map<String, Object> serialize() {
+        // Paper - diff on change
         Map<String, Object> map = new LinkedHashMap<>();
         if (this.uniqueId != null) {
             map.put("uniqueId", this.uniqueId.toString());
@@ -251,10 +254,12 @@ public final class CraftPlayerProfile implements PlayerProfile {
             });
             map.put("properties", propertiesData);
         }
+        // Paper - diff on change
         return map;
     }
 
     public static CraftPlayerProfile deserialize(Map<String, Object> map) {
+        // Paper - diff on change
         UUID uniqueId = ConfigSerializationUtil.getUuid(map, "uniqueId", true);
         String name = ConfigSerializationUtil.getString(map, "name", true);
 
@@ -270,7 +275,7 @@ public final class CraftPlayerProfile implements PlayerProfile {
                 profile.properties.put(property.getName(), property);
             }
         }
-
+        // Paper - diff on change
         return profile;
     }
 }
diff --git a/src/main/java/org/bukkit/craftbukkit/profile/CraftPlayerTextures.java b/src/main/java/org/bukkit/craftbukkit/profile/CraftPlayerTextures.java
index e5b61bc1f3a4bfccca386360c4920ffb8b768308..ab1fd3fb39bd40fb867432861462db5f866bce6f 100644
--- a/src/main/java/org/bukkit/craftbukkit/profile/CraftPlayerTextures.java
+++ b/src/main/java/org/bukkit/craftbukkit/profile/CraftPlayerTextures.java
@@ -48,7 +48,7 @@ public final class CraftPlayerTextures implements PlayerTextures {
         }
     }
 
-    private final CraftPlayerProfile profile;
+    private final com.destroystokyo.paper.profile.SharedPlayerProfile profile; // Paper
 
     // The textures data is loaded lazily:
     private boolean loaded = false;
@@ -67,7 +67,7 @@ public final class CraftPlayerTextures implements PlayerTextures {
     // GameProfiles (even if these modifications are later reverted).
     private boolean dirty = false;
 
-    CraftPlayerTextures(@Nonnull CraftPlayerProfile profile) {
+    public CraftPlayerTextures(@Nonnull com.destroystokyo.paper.profile.SharedPlayerProfile profile) { // Paper
         this.profile = profile;
     }