Last active
February 14, 2025 19:07
-
-
Save Revxrsal/98387f424c929f54de2aaf6157a0e384 to your computer and use it in GitHub Desktop.
A utility for fetching OfflinePlayers asynchronously
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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