All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.cryptomorin.xseries.profiles.builder.ProfileInstruction Maven / Gradle / Ivy

There is a newer version: 12.1.0
Show newest version
package com.cryptomorin.xseries.profiles.builder;

import com.cryptomorin.xseries.profiles.ProfilesCore;
import com.cryptomorin.xseries.profiles.exceptions.InvalidProfileException;
import com.cryptomorin.xseries.profiles.exceptions.ProfileChangeException;
import com.cryptomorin.xseries.profiles.exceptions.ProfileException;
import com.cryptomorin.xseries.profiles.mojang.PlayerProfileFetcherThread;
import com.cryptomorin.xseries.profiles.mojang.ProfileRequestConfiguration;
import com.cryptomorin.xseries.profiles.objects.ProfileContainer;
import com.cryptomorin.xseries.profiles.objects.Profileable;
import com.mojang.authlib.GameProfile;
import org.bukkit.block.BlockState;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.SkullMeta;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;

/**
 * Represents an instruction that sets a property of a {@link GameProfile}.
 * It uses a {@link #profileContainer} to define how to set the property and
 * a {@link #profileable} to define what to set in the property.
 *
 * @param  The type of the result produced by the {@link #profileContainer} function.
 */
public final class ProfileInstruction implements Profileable {
    /**
     * The function called that applies the given {@link #profileable} to an object that supports it
     * such as {@link ItemStack}, {@link SkullMeta} or a {@link BlockState}.
     */
    private final ProfileContainer profileContainer;
    /**
     * The main profile to set.
     */
    private Profileable profileable;
    /**
     * All fallback profiles to try if the main one fails.
     */
    private final List fallbacks = new ArrayList<>();
    private Consumer> onFallback;
    private ProfileRequestConfiguration profileRequestConfiguration;

    private boolean lenient = false;

    protected ProfileInstruction(ProfileContainer profileContainer) {
        this.profileContainer = profileContainer;
    }

    /**
     * Removes the profile and skin texture from the item/block.
     */
    public T removeProfile() {
        profileContainer.setProfile(null);
        return profileContainer.getObject();
    }

    @ApiStatus.Experimental
    public ProfileInstruction profileRequestConfiguration(ProfileRequestConfiguration config) {
        this.profileRequestConfiguration = config;
        return this;
    }

    /**
     * Fails silently if any of the {@link ProfileException} errors occur.
     * Mainly affects {@link Profileable#detect(String)}
     */
    public ProfileInstruction lenient() {
        this.lenient = true;
        return this;
    }

    /**
     * The current profile of the item/block (not the profile provided in {@link #profile(Profileable)})
     */
    @Override
    @Nullable
    public GameProfile getProfile() {
        return profileContainer.getProfile();
    }

    /**
     * A string representation of the {@link #getProfile()} which is useful for data storage.
     */
    @Nullable
    public String getProfileString() {
        return profileContainer.getProfileValue();
    }

    /**
     * Sets the texture profile to be set to the item/block. Use one of the
     * static methods of {@link Profileable} class.
     */
    public ProfileInstruction profile(Profileable profileable) {
        this.profileable = profileable;
        return this;
    }

    /**
     * A list of fallback profiles in order. If the profile set in {@link #profile(Profileable)} fails,
     * these profiles will be tested in order until a correct one is found,
     * also if any of the fallback profiles are used, {@link #onFallback} will be called too.
     * @see #apply()
     */
    public ProfileInstruction fallback(Profileable... fallbacks) {
        this.fallbacks.addAll(Arrays.asList(fallbacks));
        return this;
    }

    /**
     * Called when any of the {@link #fallback(Profileable...)} profiles are used,
     * this is also called if no fallback profile is provided, but the main one {@link #profile(Profileable)} fails.
     * @see #onFallback(Runnable)
     */
    public ProfileInstruction onFallback(Consumer> onFallback) {
        this.onFallback = onFallback;
        return this;
    }

    /**
     * @see #onFallback(Consumer)
     */
    public ProfileInstruction onFallback(Runnable onFallback) {
        this.onFallback = (fallback) -> onFallback.run();
        return this;
    }

    /**
     * Sets the profile generated by the instruction to the result type synchronously.
     * This is recommended if your code is already not on the main thread, or if you know
     * that the skull texture doesn't need additional requests.
     *
     * 

What are these additional requests?

* This only applies to offline mode (cracked) servers. Since these servers use * a cracked version of the player UUIDs and not their real ones, the real UUID * needs to be known by requesting it from Mojang servers and this request which * requires internet connection, will delay things a lot. * * @return The result after setting the generated profile. * @throws ProfileChangeException If any type of {@link ProfileException} occurs, they will be accumulated * in form of suppressed exceptions ({@link Exception#getSuppressed()}) in this single exception * starting from the main profile, followed by the fallback profiles. */ public T apply() { Objects.requireNonNull(profileable, "No profile was set"); ProfileChangeException exception = null; List tries = new ArrayList<>(2 + fallbacks.size()); tries.add(profileable); tries.addAll(fallbacks); if (lenient) tries.add(XSkull.getDefaultProfile()); boolean success = false; boolean tryingFallbacks = false; for (Profileable profileable : tries) { try { GameProfile gameProfile = profileable.getDisposableProfile(); if (gameProfile != null) { profileContainer.setProfile(gameProfile); success = true; break; } else { if (exception == null) { exception = new ProfileChangeException("Could not set the profile for " + profileContainer); } exception.addSuppressed(new InvalidProfileException("Profile doesn't have a value: " + profileable)); tryingFallbacks = true; } } catch (ProfileException ex) { if (exception == null) { exception = new ProfileChangeException("Could not set the profile for " + profileContainer); } exception.addSuppressed(ex); tryingFallbacks = true; } } if (exception != null) { if (success || lenient) ProfilesCore.debug("apply() silenced exception {}", exception); else throw exception; } T object = profileContainer.getObject(); if (tryingFallbacks && this.onFallback != null) { ProfileFallback fallback = new ProfileFallback<>(this, object, exception); this.onFallback.accept(fallback); object = fallback.getObject(); } return object; } /** * Asynchronously applies the instruction to generate a {@link GameProfile} and returns a {@link CompletableFuture}. * This method is designed for non-blocking execution, allowing tasks to be performed * in the background without blocking the server's main thread. * This method will always execute async, even if the results are cached. *
*

Reference Issues

* Note that while these methods apply to the item/block instances, passing these instances * to certain methods, for example {@link org.bukkit.inventory.Inventory#setItem(int, ItemStack)} * will create a NMS copy of that instance and use that instead. Which means if for example * you're going to be using an item for an inventory, you'd have to set the item again * manually to the inventory once this method is done. *
{@code
     * Inventory inventory = ...;
     * XSkull.createItem().profile(player).applyAsync()
     *     .thenAcceptAsync(item -> inventory.setItem(slot, item));
     * }
* * To make this cleaner, you could change the first line of the item's lore to something like "Loading..." * and set it to the inventory right away so the player knows that the data is not fully loaded. * Once this method is done, you could change the lore back and set the item back to the inventory. * (The lore is preferred because it has less text limit compared to the title, it also gives the player * all the textual information they need rather than the visual information if you're in a hurry) *


*

Usage example:

*
{@code
     *   XSkull.createItem().profile(player).applyAsync()
     *      .thenAcceptAsync(result -> {
     *          // Additional processing...
     *      }, runnable -> Bukkit.getScheduler().runTask(plugin, runnable));
     * }
* * @return A {@link CompletableFuture} that will complete asynchronously. */ public CompletableFuture applyAsync() { return CompletableFuture.supplyAsync(this::apply, PlayerProfileFetcherThread.EXECUTOR); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy