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

Provides basic elements of a PlayerProfile to be used by future API/events

diff --git a/src/main/java/com/destroystokyo/paper/profile/PlayerProfile.java b/src/main/java/com/destroystokyo/paper/profile/PlayerProfile.java
new file mode 100644
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/profile/PlayerProfile.java
@@ -0,0 +0,0 @@
+package com.destroystokyo.paper.profile;
+
+import java.util.Collection;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import org.bukkit.profile.PlayerTextures;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Represents a players profile for the game, such as UUID, Name, and textures.
+ */
+@NullMarked
+public interface PlayerProfile extends org.bukkit.profile.PlayerProfile {
+
+    /**
+     * @return The players name, if set
+     */
+    @Override
+    @Nullable
+    String getName();
+
+    /**
+     * Sets this profiles Name
+     *
+     * @param name The new Name
+     * @return The previous Name
+     */
+    @Deprecated(forRemoval = true, since = "1.18.1")
+    String setName(@Nullable String name);
+
+    /**
+     * @return The players unique identifier, if set
+     */
+    @Nullable
+    UUID getId();
+
+    /**
+     * Sets this profiles UUID
+     *
+     * @param uuid The new UUID
+     * @return The previous UUID
+     */
+    @Deprecated(forRemoval = true, since = "1.18.1")
+    @Nullable
+    UUID setId(@Nullable UUID uuid);
+
+    /**
+     * Gets the {@link PlayerTextures} of this profile.
+     * This will build a snapshot of the current texture data once
+     * requested inside PlayerTextures.
+     *
+     * @return the textures, not <code>null</code>
+     */
+    @Override
+    PlayerTextures getTextures();
+
+    /**
+     * Copies the given textures.
+     *
+     * @param textures the textures to copy, or <code>null</code> to clear the
+     * textures
+     */
+    @Override
+    void setTextures(@Nullable PlayerTextures textures);
+
+    /**
+     * @return A Mutable set of this players properties, such as textures.
+     * Values specified here are subject to implementation details.
+     */
+    Set<ProfileProperty> getProperties();
+
+    /**
+     * Check if the Profile has the specified property
+     *
+     * @param property Property name to check
+     * @return If the property is set
+     */
+    boolean hasProperty(@Nullable String property);
+
+    /**
+     * Sets a property. If the property already exists, the previous one will be replaced
+     *
+     * @param property Property to set.
+     * @throws IllegalArgumentException if setting the property results in more than 16 properties
+     */
+    void setProperty(ProfileProperty property);
+
+    /**
+     * Sets multiple properties. If any of the set properties already exist, it will be replaced
+     *
+     * @param properties The properties to set
+     * @throws IllegalArgumentException if the number of properties exceeds 16
+     */
+    void setProperties(Collection<ProfileProperty> properties);
+
+    /**
+     * Removes a specific property from this profile
+     *
+     * @param property The property to remove
+     * @return If a property was removed
+     */
+    boolean removeProperty(@Nullable String property);
+
+    /**
+     * Removes a specific property from this profile
+     *
+     * @param property The property to remove
+     * @return If a property was removed
+     */
+    default boolean removeProperty(final ProfileProperty property) {
+        return this.removeProperty(property.getName());
+    }
+
+    /**
+     * Removes all properties in the collection
+     *
+     * @param properties The properties to remove
+     * @return If any property was removed
+     */
+    default boolean removeProperties(final Collection<ProfileProperty> properties) {
+        boolean removed = false;
+        for (final ProfileProperty property : properties) {
+            if (this.removeProperty(property)) {
+                removed = true;
+            }
+        }
+        return removed;
+    }
+
+    /**
+     * Clears all properties on this profile
+     */
+    void clearProperties();
+
+    /**
+     * @return If the profile is now complete (has UUID and Name)
+     */
+    @Override
+    boolean isComplete();
+
+    /**
+     * Like {@link #complete(boolean)} but will try only from cache, and not make network calls
+     * Does not account for textures.
+     *
+     * @return If the profile is now complete (has UUID and Name)
+     */
+    boolean completeFromCache();
+
+    /**
+     * Like {@link #complete(boolean)} but will try only from cache, and not make network calls
+     * Does not account for textures.
+     *
+     * @param onlineMode Treat this as online mode or not
+     * @return If the profile is now complete (has UUID and Name)
+     */
+    boolean completeFromCache(boolean onlineMode);
+
+    /**
+     * Like {@link #complete(boolean)} but will try only from cache, and not make network calls
+     * Does not account for textures.
+     *
+     * @param lookupUUID If only name is supplied, should we do a UUID lookup
+     * @param onlineMode Treat this as online mode or not
+     * @return If the profile is now complete (has UUID and Name)
+     */
+    boolean completeFromCache(boolean lookupUUID, boolean onlineMode);
+
+    /**
+     * If this profile is not complete, then make the API call to complete it.
+     * This is a blocking operation and should be done asynchronously.
+     * <p>
+     * This will also complete textures. If you do not want to load textures, use {{@link #complete(boolean)}}
+     *
+     * @return If the profile is now complete (has UUID and Name) (if you get rate limited, this operation may fail)
+     */
+    default boolean complete() {
+        return this.complete(true);
+    }
+
+    /**
+     * If this profile is not complete, then make the API call to complete it.
+     * This is a blocking operation and should be done asynchronously.
+     * <p>
+     * Optionally will also fill textures.
+     * <p>
+     * Online mode will be automatically determined
+     *
+     * @param textures controls if we should fill the profile with texture properties
+     * @return If the profile is now complete (has UUID and Name) (if you get rate limited, this operation may fail)
+     */
+    boolean complete(boolean textures);
+
+    /**
+     * If this profile is not complete, then make the API call to complete it.
+     * This is a blocking operation and should be done asynchronously.
+     * <p>
+     * Optionally will also fill textures.
+     *
+     * @param textures controls if we should fill the profile with texture properties
+     * @param onlineMode Treat this server as online mode or not
+     * @return If the profile is now complete (has UUID and Name) (if you get rate limited, this operation may fail)
+     */
+    boolean complete(boolean textures, boolean onlineMode);
+
+    /**
+     * Produces an updated player profile based on this profile.
+     * <p>
+     * This tries to produce a completed profile by filling in missing
+     * properties (name, unique id, textures, etc.), and updates existing
+     * properties (e.g. name, textures, etc.) to their official and up-to-date
+     * values. This operation does not alter the current profile, but produces a
+     * new updated {@link PlayerProfile}.
+     * <p>
+     * If no player exists for the unique id or name of this profile, this
+     * operation yields a profile that is equal to the current profile, which
+     * might not be complete.
+     * <p>
+     * This is an asynchronous operation: Updating the profile can result in an
+     * outgoing connection in another thread in order to fetch the latest
+     * profile properties. The returned {@link CompletableFuture} will be
+     * completed once the updated profile is available. In order to not block
+     * the server's main thread, you should not wait for the result of the
+     * returned CompletableFuture on the server's main thread. Instead, if you
+     * want to do something with the updated player profile on the server's main
+     * thread once it is available, you could do something like this:
+     * <pre>
+     * profile.update().thenAcceptAsync(updatedProfile -> {
+     *     // Do something with the updated profile:
+     *     // ...
+     * }, runnable -> Bukkit.getScheduler().runTask(plugin, runnable));
+     * </pre>
+     */
+    @Override
+    CompletableFuture<PlayerProfile> update();
+
+    /**
+     * Whether this Profile has textures associated to it
+     *
+     * @return If it has a textures property
+     */
+    default boolean hasTextures() {
+        return this.hasProperty("textures");
+    }
+}
diff --git a/src/main/java/com/destroystokyo/paper/profile/ProfileProperty.java b/src/main/java/com/destroystokyo/paper/profile/ProfileProperty.java
new file mode 100644
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/profile/ProfileProperty.java
@@ -0,0 +0,0 @@
+package com.destroystokyo.paper.profile;
+
+import com.google.common.base.Preconditions;
+import java.util.Objects;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Represents a property on a {@link PlayerProfile}
+ */
+@NullMarked
+public final class ProfileProperty {
+
+    private final String name;
+    private final String value;
+    private final @Nullable String signature;
+
+    public ProfileProperty(final String name, final String value) {
+        this(name, value, null);
+    }
+
+    public ProfileProperty(final String name, final String value, final @Nullable String signature) {
+        this.name = Preconditions.checkNotNull(name, "ProfileProperty name can not be null");
+        this.value = Preconditions.checkNotNull(value, "ProfileProperty value can not be null");
+        this.signature = signature;
+        Preconditions.checkArgument(name.length() <= 64, "ProfileProperty name can not be longer than 64 characters");
+        Preconditions.checkArgument(value.length() <= Short.MAX_VALUE, "ProfileProperty value can not be longer than 32767 characters");
+        Preconditions.checkArgument(signature == null || signature.length() <= 1024, "ProfileProperty signature can not be longer than 1024 characters");
+    }
+
+    /**
+     * @return The property name, ie "textures"
+     */
+    public String getName() {
+        return this.name;
+    }
+
+    /**
+     * @return The property value, likely to be base64 encoded
+     */
+    public String getValue() {
+        return this.value;
+    }
+
+    /**
+     * @return A signature from Mojang for signed properties
+     */
+    public @Nullable String getSignature() {
+        return this.signature;
+    }
+
+    /**
+     * @return If this property has a signature or not
+     */
+    public boolean isSigned() {
+        return this.signature != null;
+    }
+
+    @Override
+    public boolean equals(final @Nullable Object o) {
+        if (this == o) return true;
+        if (o == null || this.getClass() != o.getClass()) return false;
+        final ProfileProperty that = (ProfileProperty) o;
+        return Objects.equals(this.name, that.name) &&
+            Objects.equals(this.value, that.value) &&
+            Objects.equals(this.signature, that.signature);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(this.name);
+    }
+}
diff --git a/src/main/java/org/bukkit/Bukkit.java b/src/main/java/org/bukkit/Bukkit.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/org/bukkit/Bukkit.java
+++ b/src/main/java/org/bukkit/Bukkit.java
@@ -0,0 +0,0 @@ public final class Bukkit {
     public static boolean suggestPlayerNamesWhenNullTabCompletions() {
         return server.suggestPlayerNamesWhenNullTabCompletions();
     }
+
+    /**
+     * Creates a PlayerProfile for the specified uuid, with name as null.
+     *
+     * If a player with the passed uuid exists on the server at the time of creation, the returned player profile will
+     * be populated with the properties of said player (including their uuid and name).
+     *
+     * @param uuid UUID to create profile for
+     * @return A PlayerProfile object
+     */
+    @NotNull
+    public static com.destroystokyo.paper.profile.PlayerProfile createProfile(@NotNull UUID uuid) {
+        return server.createProfile(uuid);
+    }
+
+    /**
+     * Creates a PlayerProfile for the specified name, with UUID as null.
+     *
+     * If a player with the passed name exists on the server at the time of creation, the returned player profile will
+     * be populated with the properties of said player (including their uuid and name).
+     * <p>
+     * E.g. if the player 'jeb_' is currently playing on the server, calling {@code createProfile("JEB_")} will
+     * yield a profile with the name 'jeb_', their uuid and their textures.
+     * To bypass this pre-population on a case-insensitive name match, see {@link #createProfileExact(UUID, String)}.
+     * <p>
+     *
+     * @param name Name to create profile for
+     * @return A PlayerProfile object
+     * @throws IllegalArgumentException if the name is longer than 16 characters
+     * @throws IllegalArgumentException if the name contains invalid characters
+     */
+    @NotNull
+    public static com.destroystokyo.paper.profile.PlayerProfile createProfile(@NotNull String name) {
+        return server.createProfile(name);
+    }
+
+    /**
+     * Creates a PlayerProfile for the specified name/uuid
+     *
+     * Both UUID and Name can not be null at same time. One must be supplied.
+     * If a player with the passed uuid or name exists on the server at the time of creation, the returned player
+     * profile will be populated with the properties of said player (including their uuid and name).
+     * <p>
+     * E.g. if the player 'jeb_' is currently playing on the server, calling {@code createProfile(null, "JEB_")} will
+     * yield a profile with the name 'jeb_', their uuid and their textures.
+     * To bypass this pre-population on an case-insensitive name match, see {@link #createProfileExact(UUID, String)}.
+     * <p>
+     *
+     * The name comparison will compare the {@link String#toLowerCase()} version of both the passed name parameter and
+     * a players name to honour the case-insensitive nature of a mojang profile lookup.
+     *
+     * @param uuid UUID to create profile for
+     * @param name Name to create profile for
+     * @return A PlayerProfile object
+     * @throws IllegalArgumentException if the name is longer than 16 characters
+     * @throws IllegalArgumentException if the name contains invalid characters
+     */
+    @NotNull
+    public static com.destroystokyo.paper.profile.PlayerProfile createProfile(@Nullable UUID uuid, @Nullable String name) {
+        return server.createProfile(uuid, name);
+    }
+
+    /**
+     * Creates an exact PlayerProfile for the specified name/uuid
+     *
+     * Both UUID and Name can not be null at same time. One must be supplied.
+     * If a player with the passed uuid or name exists on the server at the time of creation, the returned player
+     * profile will be populated with the properties of said player.
+     * <p>
+     * Compared to {@link #createProfile(UUID, String)}, this method will never mutate the passed uuid or name.
+     * If a player with either the same uuid or a matching name (case-insensitive) is found on the server, their
+     * properties, such as textures, will be pre-populated in the profile, however the passed uuid and name stay intact.
+     *
+     * @param uuid UUID to create profile for
+     * @param name Name to create profile for
+     * @return A PlayerProfile object
+     * @throws IllegalArgumentException if the name is longer than 16 characters
+     * @throws IllegalArgumentException if the name contains invalid characters
+     */
+    @NotNull
+    public static com.destroystokyo.paper.profile.PlayerProfile createProfileExact(@Nullable UUID uuid, @Nullable String name) {
+        return server.createProfileExact(uuid, name);
+    }
     // Paper end
 
     @NotNull
diff --git a/src/main/java/org/bukkit/Server.java b/src/main/java/org/bukkit/Server.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/org/bukkit/Server.java
+++ b/src/main/java/org/bukkit/Server.java
@@ -0,0 +0,0 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi
      * @return true if player names should be suggested
      */
     boolean suggestPlayerNamesWhenNullTabCompletions();
+
+    /**
+     * Creates a PlayerProfile for the specified uuid, with name as null.
+     *
+     * If a player with the passed uuid exists on the server at the time of creation, the returned player profile will
+     * be populated with the properties of said player (including their uuid and name).
+     *
+     * @param uuid UUID to create profile for
+     * @return A PlayerProfile object
+     */
+    @NotNull
+    com.destroystokyo.paper.profile.PlayerProfile createProfile(@NotNull UUID uuid);
+
+    /**
+     * Creates a PlayerProfile for the specified name, with UUID as null.
+     *
+     * If a player with the passed name exists on the server at the time of creation, the returned player profile will
+     * be populated with the properties of said player (including their uuid and name).
+     * <p>
+     * E.g. if the player 'jeb_' is currently playing on the server, calling {@code createProfile("JEB_")} will
+     * yield a profile with the name 'jeb_', their uuid and their textures.
+     * To bypass this pre-population on a case-insensitive name match, see {@link #createProfileExact(UUID, String)}.
+     * <p>
+     *
+     * @param name Name to create profile for
+     * @return A PlayerProfile object
+     * @throws IllegalArgumentException if the name is longer than 16 characters
+     * @throws IllegalArgumentException if the name contains invalid characters
+     */
+    @NotNull
+    com.destroystokyo.paper.profile.PlayerProfile createProfile(@NotNull String name);
+
+    /**
+     * Creates a PlayerProfile for the specified name/uuid
+     *
+     * Both UUID and Name can not be null at same time. One must be supplied.
+     * If a player with the passed uuid or name exists on the server at the time of creation, the returned player
+     * profile will be populated with the properties of said player (including their uuid and name).
+     * <p>
+     * E.g. if the player 'jeb_' is currently playing on the server, calling {@code createProfile(null, "JEB_")} will
+     * yield a profile with the name 'jeb_', their uuid and their textures.
+     * To bypass this pre-population on an case-insensitive name match, see {@link #createProfileExact(UUID, String)}.
+     * <p>
+     *
+     * The name comparison will compare the {@link String#toLowerCase()} version of both the passed name parameter and
+     * a players name to honour the case-insensitive nature of a mojang profile lookup.
+     *
+     * @param uuid UUID to create profile for
+     * @param name Name to create profile for
+     * @return A PlayerProfile object
+     * @throws IllegalArgumentException if the name is longer than 16 characters
+     * @throws IllegalArgumentException if the name contains invalid characters
+     */
+    @NotNull
+    com.destroystokyo.paper.profile.PlayerProfile createProfile(@Nullable UUID uuid, @Nullable String name);
+
+    /**
+     * Creates an exact PlayerProfile for the specified name/uuid
+     *
+     * Both UUID and Name can not be null at same time. One must be supplied.
+     * If a player with the passed uuid or name exists on the server at the time of creation, the returned player
+     * profile will be populated with the properties of said player.
+     * <p>
+     * Compared to {@link #createProfile(UUID, String)}, this method will never mutate the passed uuid or name.
+     * If a player with either the same uuid or a matching name (case-insensitive) is found on the server, their
+     * properties, such as textures, will be pre-populated in the profile, however the passed uuid and name stay intact.
+     *
+     * @param uuid UUID to create profile for
+     * @param name Name to create profile for
+     * @return A PlayerProfile object
+     * @throws IllegalArgumentException if the name is longer than 16 characters
+     * @throws IllegalArgumentException if the name contains invalid characters
+     */
+    @NotNull
+    com.destroystokyo.paper.profile.PlayerProfile createProfileExact(@Nullable UUID uuid, @Nullable String name);
     // Paper end
 }
diff --git a/src/main/java/org/bukkit/profile/PlayerProfile.java b/src/main/java/org/bukkit/profile/PlayerProfile.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/org/bukkit/profile/PlayerProfile.java
+++ b/src/main/java/org/bukkit/profile/PlayerProfile.java
@@ -0,0 +0,0 @@ public interface PlayerProfile extends Cloneable, ConfigurationSerializable {
      * PlayerProfile once it is available
      */
     @NotNull
-    CompletableFuture<PlayerProfile> update();
+    CompletableFuture<? extends PlayerProfile> update(); // Paper
 
     @NotNull
     PlayerProfile clone();