Skip to content

Instantly share code, notes, and snippets.

@Revxrsal
Last active February 14, 2025 19:07
Show Gist options
  • Save Revxrsal/98387f424c929f54de2aaf6157a0e384 to your computer and use it in GitHub Desktop.
Save Revxrsal/98387f424c929f54de2aaf6157a0e384 to your computer and use it in GitHub Desktop.
A utility for fetching OfflinePlayers asynchronously
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import revxrsal.commands.autocomplete.SuggestionProvider;
import revxrsal.commands.bukkit.exception.InvalidPlayerException;
import revxrsal.commands.command.CommandActor;
import revxrsal.commands.node.ExecutionContext;
import revxrsal.commands.parameter.ParameterType;
import revxrsal.commands.stream.MutableStringStream;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* Represents an asynchronously fetched {@link OfflinePlayer}.
* Provides caching and non-blocking retrieval.
*/
public abstract class AsyncOfflinePlayer {
private static final Cache<String, AsyncOfflinePlayer> PLAYER_CACHE = CacheBuilder.newBuilder()
.expireAfterAccess(6, TimeUnit.HOURS)
.build();
/**
* Returns the player's name.
*
* @return The player's name.
*/
@NotNull
public abstract String getName();
/**
* Returns the player if already fetched, otherwise {@code null}.
*
* @return The fetched {@link OfflinePlayer} or {@code null}.
*/
@Nullable
public abstract OfflinePlayer getIfFetched();
/**
* Returns the player, fetching if necessary.
*
* @return The {@link OfflinePlayer} instance.
*/
@NotNull
@Blocking
public abstract OfflinePlayer getOrFetch();
/**
* Asynchronously fetches the player.
*
* @return A {@link CompletableFuture} resolving to the {@link OfflinePlayer}.
*/
@NotNull
public abstract CompletableFuture<OfflinePlayer> getOrFetchAsync();
/**
* Enum representing whether a player has played before.
*/
public enum HasPlayedBefore {
YES, NO, UNKNOWN;
/**
* Returns {@code true} if the player has played before, otherwise throws an exception.
*
* @return {@code true} if the player has played before.
* @throws IllegalStateException If the status is {@code UNKNOWN}.
*/
public boolean get() {
if (this == YES) return true;
if (this == NO) return false;
throw new IllegalStateException("Status unknown.");
}
/**
* Returns {@code Boolean.TRUE} if the player has played before, {@code Boolean.FALSE} if not, or {@code null} if unknown.
*
* @return Boolean value representing the status.
*/
public @Nullable Boolean getOrNull() {
return this == YES ? Boolean.TRUE : this == NO ? Boolean.FALSE : null;
}
}
/**
* Determines if the player has played before.
*
* @return The {@link HasPlayedBefore} status.
*/
@NotNull
public HasPlayedBefore hasPlayedBefore() {
CompletableFuture<Boolean> future = hasPlayedBeforeAsync();
if (future.isDone()) {
try {
return future.get() ? HasPlayedBefore.YES : HasPlayedBefore.NO;
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
return HasPlayedBefore.UNKNOWN;
}
/**
* Asynchronously determines if the player has played before.
*
* @return A {@link CompletableFuture} resolving to a boolean.
*/
@NotNull
public abstract CompletableFuture<Boolean> hasPlayedBeforeAsync();
/**
* Retrieves an {@link AsyncOfflinePlayer} from cache or fetches it.
*
* @param name The player's name.
* @return The {@link AsyncOfflinePlayer} instance.
*/
public static @NotNull AsyncOfflinePlayer from(@NotNull String name) {
Player player = Bukkit.getPlayer(name);
if (player != null) return new Resolved(player);
try {
return PLAYER_CACHE.get(name, () -> AsyncOfflinePlayer.fetchAsync(name));
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
/**
* Wraps an existing {@link OfflinePlayer} into an {@link AsyncOfflinePlayer}.
*
* @param player The offline player.
* @return The wrapped {@link AsyncOfflinePlayer}.
*/
public static @NotNull AsyncOfflinePlayer from(@NotNull OfflinePlayer player) {
return new Resolved(player);
}
/**
* Fetches an {@link AsyncOfflinePlayer} asynchronously and caches the result.
*
* @param name The player's name.
* @return The fetched {@link AsyncOfflinePlayer}.
*/
public static @NotNull AsyncOfflinePlayer fetchAsync(@NotNull String name) {
CompletableFuture<OfflinePlayer> future = CompletableFuture.supplyAsync(() -> Bukkit.getOfflinePlayer(name));
Fetching player = new Fetching(name, future);
PLAYER_CACHE.put(name, player);
return player;
}
/**
* Returns the Lamp {@link ParameterType} for parsing {@link AsyncOfflinePlayer} instances.
*
* @return The parameter type instance.
*/
public static @NotNull ParameterType<CommandActor, AsyncOfflinePlayer> parameterType() {
return LampParameterType.INSTANCE;
}
/**
* Represents an {@link AsyncOfflinePlayer} that has already been resolved.
*/
private static class Resolved extends AsyncOfflinePlayer {
private final OfflinePlayer player;
public Resolved(OfflinePlayer player) {
this.player = player;
}
@Override
public @NotNull String getName() {
return Objects.requireNonNull(player.getName(), "player.getName() is null!");
}
@Override
public @Nullable OfflinePlayer getIfFetched() {
return player;
}
@Override
public @NotNull OfflinePlayer getOrFetch() {
return player;
}
@Override
public @NotNull AsyncOfflinePlayer.HasPlayedBefore hasPlayedBefore() {
// feel free to add a more sophisticated implementation here
return player.hasPlayedBefore() ? HasPlayedBefore.YES : HasPlayedBefore.NO;
}
@Override
public @NotNull CompletableFuture<OfflinePlayer> getOrFetchAsync() {
return CompletableFuture.completedFuture(player);
}
@Override
public @NotNull CompletableFuture<Boolean> hasPlayedBeforeAsync() {
// feel free to add a more sophisticated implementation here
return CompletableFuture.completedFuture(player.hasPlayedBefore());
}
}
/**
* Represents an {@link AsyncOfflinePlayer} that is being fetched. It may
* have been fetched and is ready.
*/
private static class Fetching extends AsyncOfflinePlayer {
private final String name;
private final CompletableFuture<OfflinePlayer> future;
public Fetching(String name, CompletableFuture<OfflinePlayer> future) {
this.name = name;
this.future = future;
}
@Override
public @NotNull String getName() {
return name;
}
@Override
public @Nullable OfflinePlayer getIfFetched() {
if (future.isDone()) {
try {
return future.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
return null;
}
@Override
public @NotNull OfflinePlayer getOrFetch() {
try {
return future.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
@Override
public @NotNull CompletableFuture<OfflinePlayer> getOrFetchAsync() {
return future;
}
@Override
public @NotNull CompletableFuture<Boolean> hasPlayedBeforeAsync() {
return future.thenApply(OfflinePlayer::hasPlayedBefore);
}
}
private static class LampParameterType implements ParameterType<CommandActor, AsyncOfflinePlayer> {
private static final LampParameterType INSTANCE = new LampParameterType();
@Override
public AsyncOfflinePlayer parse(@NotNull MutableStringStream input, @NotNull ExecutionContext<@NotNull CommandActor> context) {
String name = input.readUnquotedString();
AsyncOfflinePlayer player = AsyncOfflinePlayer.from(name);
if (player.hasPlayedBefore() == HasPlayedBefore.NO) {
throw new InvalidPlayerException(name);
}
return player;
}
@Override
public @NotNull SuggestionProvider<@NotNull CommandActor> defaultSuggestions() {
return context -> {
Set<String> names = new HashSet<>();
for (Player player : Bukkit.getOnlinePlayers())
names.add(player.getName());
names.addAll(PLAYER_CACHE.asMap().keySet());
return names;
};
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment