From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Aikar 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..324c1cba46c9eef95cc22ffa231b04f9298a5e00 --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/profile/PlayerProfile.java @@ -0,0 +1,246 @@ +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 null + */ + @Override + PlayerTextures getTextures(); + + /** + * Copies the given textures. + * + * @param textures the textures to copy, or null 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 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 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 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. + *

+ * 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. + *

+ * Optionally will also fill textures. + *

+ * 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. + *

+ * 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. + *

+ * 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}. + *

+ * 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. + *

+ * 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: + *

+     * profile.update().thenAcceptAsync(updatedProfile -> {
+     *     // Do something with the updated profile:
+     *     // ...
+     * }, runnable -> Bukkit.getScheduler().runTask(plugin, runnable));
+     * 
+ */ + @Override + CompletableFuture 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..35341d8f1ac2d80f339084ef80d099a545027554 --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/profile/ProfileProperty.java @@ -0,0 +1,73 @@ +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 c900bde81d8f20ed4775228fbe767d7db62508fa..6c44f58551dda4129d37cce100a9c5b9c9810cca 100644 --- a/src/main/java/org/bukkit/Bukkit.java +++ b/src/main/java/org/bukkit/Bukkit.java @@ -2424,6 +2424,89 @@ 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). + *

+ * 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)}. + *

+ * + * @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). + *

+ * 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)}. + *

+ * + * 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. + *

+ * 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 0588c6c8b30bfe6e1e8ab7e1523a8a9be6b11b6f..55d8602bb651f85b9d0a80ec015cec5e48948d44 100644 --- a/src/main/java/org/bukkit/Server.java +++ b/src/main/java/org/bukkit/Server.java @@ -2108,5 +2108,80 @@ 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). + *

+ * 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)}. + *

+ * + * @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). + *

+ * 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)}. + *

+ * + * 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. + *

+ * 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 16ae1282f3178e8873483a25a5d5cce16b2c21a9..fc46add38bf59dc1a04ea566fd230dcd8ae2708c 100644 --- a/src/main/java/org/bukkit/profile/PlayerProfile.java +++ b/src/main/java/org/bukkit/profile/PlayerProfile.java @@ -93,7 +93,7 @@ public interface PlayerProfile extends Cloneable, ConfigurationSerializable { * PlayerProfile once it is available */ @NotNull - CompletableFuture update(); + CompletableFuture update(); // Paper @NotNull PlayerProfile clone();