diff --git a/core/src/main/java/tc/oc/pgm/PGMPlugin.java b/core/src/main/java/tc/oc/pgm/PGMPlugin.java index 73aa228373..488d2a9daa 100644 --- a/core/src/main/java/tc/oc/pgm/PGMPlugin.java +++ b/core/src/main/java/tc/oc/pgm/PGMPlugin.java @@ -44,6 +44,7 @@ import tc.oc.pgm.command.util.PGMCommandGraph; import tc.oc.pgm.db.CacheDatastore; import tc.oc.pgm.db.SQLDatastore; +import tc.oc.pgm.db.SqlUsernameResolver; import tc.oc.pgm.integrations.SimpleVanishIntegration; import tc.oc.pgm.listeners.AntiGriefListener; import tc.oc.pgm.listeners.BlockTransformListener; @@ -81,6 +82,9 @@ import tc.oc.pgm.util.tablist.TablistResizer; import tc.oc.pgm.util.text.TextException; import tc.oc.pgm.util.text.TextTranslations; +import tc.oc.pgm.util.usernames.ApiUsernameResolver; +import tc.oc.pgm.util.usernames.BukkitUsernameResolver; +import tc.oc.pgm.util.usernames.UsernameResolvers; import tc.oc.pgm.util.xml.InvalidXMLException; public class PGMPlugin extends JavaPlugin implements PGM, Listener { @@ -162,6 +166,11 @@ public void onEnable() { return; } + UsernameResolvers.setResolvers( + new BukkitUsernameResolver(), + new SqlUsernameResolver((SQLDatastore) datastore), + new ApiUsernameResolver()); + datastore = new CacheDatastore(datastore); try { diff --git a/core/src/main/java/tc/oc/pgm/api/player/Username.java b/core/src/main/java/tc/oc/pgm/api/player/Username.java index 4c5802d88f..7d6f786203 100644 --- a/core/src/main/java/tc/oc/pgm/api/player/Username.java +++ b/core/src/main/java/tc/oc/pgm/api/player/Username.java @@ -22,11 +22,4 @@ public interface Username extends Named { @Override @Nullable String getNameLegacy(); - - /** - * Change the username of the player. - * - * @param name The new username or null. - */ - void setName(@Nullable String name); } diff --git a/core/src/main/java/tc/oc/pgm/db/SQLDatastore.java b/core/src/main/java/tc/oc/pgm/db/SQLDatastore.java index a17ca18b90..6ce7f02cc1 100644 --- a/core/src/main/java/tc/oc/pgm/db/SQLDatastore.java +++ b/core/src/main/java/tc/oc/pgm/db/SQLDatastore.java @@ -1,11 +1,17 @@ package tc.oc.pgm.db; +import static tc.oc.pgm.util.Assert.assertNotNull; +import static tc.oc.pgm.util.player.PlayerComponent.player; + import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.time.Duration; +import java.time.Instant; import java.util.UUID; import java.util.concurrent.ExecutionException; +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; import org.jetbrains.annotations.Nullable; import tc.oc.pgm.api.Datastore; import tc.oc.pgm.api.map.MapActivity; @@ -14,8 +20,11 @@ import tc.oc.pgm.api.setting.SettingValue; import tc.oc.pgm.api.setting.Settings; import tc.oc.pgm.util.concurrent.ThreadSafeConnection; +import tc.oc.pgm.util.named.NameStyle; import tc.oc.pgm.util.skin.Skin; import tc.oc.pgm.util.text.TextParser; +import tc.oc.pgm.util.usernames.UsernameResolver; +import tc.oc.pgm.util.usernames.UsernameResolvers; public class SQLDatastore extends ThreadSafeConnection implements Datastore { @@ -31,54 +40,44 @@ public SQLDatastore(String uri, int maxConnections) throws SQLException { "CREATE TABLE IF NOT EXISTS pools (name VARCHAR(255) PRIMARY KEY, next_map VARCHAR(255), last_active BOOLEAN)"); } - private class SQLUsername extends UsernameImpl { + private class SQLUsername implements Username { + private final Duration ONE_WEEK = Duration.ofDays(7); - private volatile boolean queried; + private final UUID id; + private String name; + private Instant validUntil; - SQLUsername(UUID id, @Nullable String name) { - super(id, name); + SQLUsername(UUID id) { + this.id = assertNotNull(id, "username id is null"); + UsernameResolvers.resolve(id).thenAccept(this::setName); } @Override - public String getNameLegacy() { - String name = super.getNameLegacy(); - - // Since there can be hundreds of names, only query when requested. - if (!queried && name == null) { - queried = true; - submitQuery(new SelectQuery()); - } + public UUID getId() { + return id; + } + @Override + public String getNameLegacy() { return name; } @Override - public void setName(@Nullable String name) { - super.setName(name); - - if (name != null) { - submitQuery(new UpdateQuery()); - } + public Component getName(NameStyle style) { + return player(Bukkit.getPlayer(id), name, style); } - private class SelectQuery implements Query { - @Override - public String getFormat() { - return "SELECT name, expires FROM usernames WHERE id = ? LIMIT 1"; - } - - @Override - public void query(PreparedStatement statement) throws SQLException { - statement.setString(1, getId().toString()); - - try (final ResultSet result = statement.executeQuery()) { - if (!result.next()) return; - - setName(result.getString(1)); - - if (result.getLong(2) < System.currentTimeMillis()) { - setName(null); - } + protected void setName(UsernameResolver.UsernameResponse response) { + // A name is provided and either we know no name, or it's more recent + if (response.getUsername() != null + && (this.name == null || response.getValidUntil().isAfter(this.validUntil))) { + this.name = response.getUsername(); + this.validUntil = response.getValidUntil(); + + // Only update names with about over a week of validity + if (response.getSource() != SqlUsernameResolver.class + && validUntil.isAfter(Instant.now().plus(ONE_WEEK))) { + submitQuery(new UpdateQuery()); } } } @@ -91,14 +90,9 @@ public String getFormat() { @Override public void query(PreparedStatement statement) throws SQLException { - statement.setString(1, getId().toString()); - statement.setString(2, getNameLegacy()); - - // Pick a random expiration time between 1 and 2 weeks - statement.setLong( - 3, - System.currentTimeMillis() + Duration.ofDays(7 + (int) (Math.random() * 7)).toMillis()); - + statement.setString(1, id.toString()); + statement.setString(2, name); + statement.setLong(3, validUntil.toEpochMilli()); statement.executeUpdate(); } } @@ -106,7 +100,7 @@ public void query(PreparedStatement statement) throws SQLException { @Override public Username getUsername(UUID id) { - return new SQLUsername(id, null); + return new SQLUsername(id); } private class SQLSettings extends SettingsImpl { diff --git a/core/src/main/java/tc/oc/pgm/db/SqlUsernameResolver.java b/core/src/main/java/tc/oc/pgm/db/SqlUsernameResolver.java new file mode 100644 index 0000000000..44a9c011d3 --- /dev/null +++ b/core/src/main/java/tc/oc/pgm/db/SqlUsernameResolver.java @@ -0,0 +1,113 @@ +package tc.oc.pgm.db; + +import com.google.common.collect.Lists; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.Instant; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import tc.oc.pgm.util.concurrent.ThreadSafeConnection; +import tc.oc.pgm.util.usernames.AbstractBatchingUsernameResolver; + +public class SqlUsernameResolver extends AbstractBatchingUsernameResolver { + private static final int BATCH_SIZE = 500; + private final SQLDatastore datastore; + + public SqlUsernameResolver(SQLDatastore datastore) { + this.datastore = datastore; + } + + protected void process(UUID uuid, CompletableFuture future) { + datastore.submitQuery(new SingleSelect(uuid, future)).join(); + } + + @Override + protected void process(List uuids) { + List> partitions = Lists.partition(uuids, BATCH_SIZE); + CompletableFuture[] futures = new CompletableFuture[partitions.size()]; + for (int i = 0; i < partitions.size(); i++) { + futures[i] = datastore.submitQuery(new BatchSelect(partitions.get(i), this::complete)); + } + CompletableFuture.allOf(futures).join(); + } + + private static class SingleSelect implements ThreadSafeConnection.Query { + private final UUID uuid; + private final CompletableFuture future; + + public SingleSelect(UUID uuid, CompletableFuture future) { + this.uuid = uuid; + this.future = future; + } + + @Override + public String getFormat() { + return "SELECT name, expires FROM usernames WHERE id = ? LIMIT 1"; + } + + @Override + public void query(PreparedStatement statement) throws SQLException { + statement.setString(1, uuid.toString()); + + try (final ResultSet result = statement.executeQuery()) { + future.complete( + !result.next() + ? UsernameResponse.empty() + : UsernameResponse.of( + result.getString(1), + null, + Instant.ofEpochMilli(result.getLong(2)), + SqlUsernameResolver.class)); + } + } + } + + private static class BatchSelect implements ThreadSafeConnection.Query { + private final List uuids; + private final BiConsumer completion; + + public BatchSelect(List uuids, BiConsumer completion) { + this.uuids = uuids; + this.completion = completion; + } + + @Override + public String getFormat() { + return "SELECT id, name, expires FROM usernames WHERE id IN (" + + Stream.generate(() -> "?").limit(uuids.size()).collect(Collectors.joining(",")) + + ")"; + } + + @Override + public void query(PreparedStatement statement) throws SQLException { + for (int i = 0; i < uuids.size(); i++) { + statement.setString(i + 1, uuids.get(i).toString()); + } + + try (final ResultSet result = statement.executeQuery()) { + Set leftover = new HashSet<>(uuids); + while (result.next()) { + UUID uuid = UUID.fromString(result.getString(1)); + leftover.remove(uuid); + + completion.accept( + uuid, + UsernameResponse.of( + result.getString(2), + null, + Instant.ofEpochMilli(result.getLong(3)), + SqlUsernameResolver.class)); + } + + leftover.forEach(uuid -> completion.accept(uuid, UsernameResponse.empty())); + } + } + } +} diff --git a/core/src/main/java/tc/oc/pgm/db/UsernameImpl.java b/core/src/main/java/tc/oc/pgm/db/UsernameImpl.java deleted file mode 100644 index 189ddc5197..0000000000 --- a/core/src/main/java/tc/oc/pgm/db/UsernameImpl.java +++ /dev/null @@ -1,65 +0,0 @@ -package tc.oc.pgm.db; - -import static tc.oc.pgm.util.Assert.assertNotNull; -import static tc.oc.pgm.util.player.PlayerComponent.player; - -import java.util.UUID; -import net.kyori.adventure.text.Component; -import org.bukkit.Bukkit; -import org.jetbrains.annotations.Nullable; -import tc.oc.pgm.api.player.Username; -import tc.oc.pgm.util.UsernameResolver; -import tc.oc.pgm.util.named.NameStyle; - -class UsernameImpl implements Username { - - private final UUID id; - private String name; - - UsernameImpl(UUID id, @Nullable String name) { - this.id = assertNotNull(id, "username id is null"); - setName(name); - } - - @Override - public final UUID getId() { - return id; - } - - @Nullable - @Override - public String getNameLegacy() { - return name; - } - - @Override - public Component getName(NameStyle style) { - return player(Bukkit.getPlayer(id), name, style); - } - - @Override - public void setName(@Nullable String name) { - if (name == null) { - UsernameResolver.resolve(id, this::setName); - } else { - this.name = name; - } - } - - @Override - public int hashCode() { - return id.hashCode(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof Username)) return false; - return getId().equals(((Username) o).getId()); - } - - @Override - public String toString() { - return name == null ? id.toString() : name; - } -} diff --git a/core/src/main/java/tc/oc/pgm/map/MapLibraryImpl.java b/core/src/main/java/tc/oc/pgm/map/MapLibraryImpl.java index 339ddd6e33..da75026689 100644 --- a/core/src/main/java/tc/oc/pgm/map/MapLibraryImpl.java +++ b/core/src/main/java/tc/oc/pgm/map/MapLibraryImpl.java @@ -29,7 +29,7 @@ import tc.oc.pgm.api.map.includes.MapIncludeProcessor; import tc.oc.pgm.util.LiquidMetal; import tc.oc.pgm.util.StringUtils; -import tc.oc.pgm.util.UsernameResolver; +import tc.oc.pgm.util.usernames.UsernameResolvers; public class MapLibraryImpl implements MapLibrary { @@ -129,7 +129,8 @@ public CompletableFuture loadNewMaps(boolean reset) { final int oldFail = failed.size(); final int oldOk = reset ? 0 : maps.size(); - return CompletableFuture.runAsync( + return CompletableFuture.runAsync(UsernameResolvers::startBatch) + .thenRunAsync( () -> { // First ensure loadNewSources is called for all factories, this may take some time // (eg: Git pull) @@ -168,7 +169,7 @@ public CompletableFuture loadNewMaps(boolean reset) { } }) .thenRunAsync(() -> logMapSuccess(oldFail, oldOk)) - .thenRunAsync(UsernameResolver::resolveAll); + .thenRunAsync(UsernameResolvers::endBatch); } @Override diff --git a/core/src/main/java/tc/oc/pgm/namedecorations/NameDecorationRegistryImpl.java b/core/src/main/java/tc/oc/pgm/namedecorations/NameDecorationRegistryImpl.java index ba8c229e7d..af62237007 100644 --- a/core/src/main/java/tc/oc/pgm/namedecorations/NameDecorationRegistryImpl.java +++ b/core/src/main/java/tc/oc/pgm/namedecorations/NameDecorationRegistryImpl.java @@ -1,6 +1,7 @@ package tc.oc.pgm.namedecorations; import static net.kyori.adventure.text.Component.text; +import static tc.oc.pgm.util.player.PlayerRenderer.OFFLINE_COLOR; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; @@ -124,7 +125,7 @@ public String getSuffix(UUID uuid) { public TextColor getColor(UUID uuid) { MatchPlayer player = PGM.get().getMatchManager().getPlayer(uuid); - if (player == null) return PlayerComponent.OFFLINE_COLOR; + if (player == null) return OFFLINE_COLOR; return TextFormatter.convert(player.getParty().getColor()); } diff --git a/core/src/main/java/tc/oc/pgm/stats/StatsMatchModule.java b/core/src/main/java/tc/oc/pgm/stats/StatsMatchModule.java index ce4ae122fb..809005da33 100644 --- a/core/src/main/java/tc/oc/pgm/stats/StatsMatchModule.java +++ b/core/src/main/java/tc/oc/pgm/stats/StatsMatchModule.java @@ -20,7 +20,6 @@ import java.util.List; import java.util.Map; import java.util.UUID; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; @@ -73,10 +72,10 @@ import tc.oc.pgm.teams.Team; import tc.oc.pgm.tracker.TrackerMatchModule; import tc.oc.pgm.tracker.info.ProjectileInfo; -import tc.oc.pgm.util.UsernameResolver; import tc.oc.pgm.util.named.NameStyle; import tc.oc.pgm.util.nms.NMSHacks; import tc.oc.pgm.util.text.TextFormatter; +import tc.oc.pgm.util.usernames.UsernameResolvers; import tc.oc.pgm.wool.MonumentWool; import tc.oc.pgm.wool.PlayerWoolPlaceEvent; @@ -313,11 +312,10 @@ public void onMatchEnd(MatchFinishEvent event) { // Try to ensure that usernames for all relevant offline players will be loaded in the cache // when the inventory GUI is created. If usernames needs to be resolved using the mojang api - // (UsernameResolver) - // it can take some time, and we cant really know how long. - this.getOfflinePlayersWithStats() - .forEach(id -> PGM.get().getDatastore().getUsername(id).getNameLegacy()); - CompletableFuture.runAsync(UsernameResolver::resolveAll); + // (UsernameResolver) it can take some time, and we cant really know how long. + UsernameResolvers.startBatch(); + this.getOfflinePlayersWithStats().forEach(id -> PGM.get().getDatastore().getUsername(id)); + UsernameResolvers.endBatch(); // Schedule displaying stats after match end match diff --git a/core/src/main/java/tc/oc/pgm/util/player/PlayerComponent.java b/core/src/main/java/tc/oc/pgm/util/player/PlayerComponent.java index b5c5735ebf..9ece453b7d 100644 --- a/core/src/main/java/tc/oc/pgm/util/player/PlayerComponent.java +++ b/core/src/main/java/tc/oc/pgm/util/player/PlayerComponent.java @@ -4,7 +4,6 @@ import java.util.UUID; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.Style; import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.format.TextDecoration; @@ -22,25 +21,24 @@ /** PlayerComponent is used to format player names in a consistent manner with optional styling */ public final class PlayerComponent implements RenderableComponent { - public static final TextColor OFFLINE_COLOR = NamedTextColor.DARK_AQUA; - public static final Component UNKNOWN = - translatable("misc.unknown", OFFLINE_COLOR, TextDecoration.ITALIC); - public static final Component CONSOLE = translatable("misc.console", OFFLINE_COLOR); + translatable("misc.unknown", PlayerRenderer.OFFLINE_COLOR, TextDecoration.ITALIC); + public static final Component CONSOLE = + translatable("misc.console", PlayerRenderer.OFFLINE_COLOR); public static final PlayerComponent UNKNOWN_PLAYER = - new PlayerComponent(null, new PlayerData(null, null, NameStyle.PLAIN)); + new PlayerComponent(null, new PlayerData(null, null, NameStyle.SIMPLE_COLOR), Style.empty()); public static final PlayerRenderer RENDERER = new PlayerRenderer(); // The data for player being rendered. private final @Nullable Player player; private final @NotNull PlayerData data; + private final Style style; - private Style style = Style.empty(); - - private PlayerComponent(@Nullable Player player, @NotNull PlayerData data) { + private PlayerComponent(@Nullable Player player, @NotNull PlayerData data, Style style) { this.player = player; this.data = data; + this.style = style; } public static Component player(@Nullable UUID playerId, @NotNull NameStyle style) { @@ -59,24 +57,25 @@ public static Component player(CommandSender sender, @NotNull NameStyle style) { public static PlayerComponent player(Player player, @NotNull NameStyle style) { if (player == null) return UNKNOWN_PLAYER; - return new PlayerComponent(player, new PlayerData(player, style)); + return new PlayerComponent(player, new PlayerData(player, style), Style.empty()); } public static Component player( @Nullable Player player, @Nullable String username, @NotNull NameStyle style) { if (player == null && username == null) return UNKNOWN_PLAYER; - return new PlayerComponent(player, new PlayerData(player, username, style)); + return new PlayerComponent(player, new PlayerData(player, username, style), Style.empty()); } public static PlayerComponent player( @Nullable MatchPlayerState player, @NotNull NameStyle style) { if (player == null) return UNKNOWN_PLAYER; - return new PlayerComponent(Bukkit.getPlayer(player.getId()), new PlayerData(player, style)); + return new PlayerComponent( + Bukkit.getPlayer(player.getId()), new PlayerData(player, style), Style.empty()); } public static PlayerComponent player(@Nullable MatchPlayer player, @NotNull NameStyle style) { if (player == null) return UNKNOWN_PLAYER; - return new PlayerComponent(player.getBukkit(), new PlayerData(player, style)); + return new PlayerComponent(player.getBukkit(), new PlayerData(player, style), Style.empty()); } public Component render(CommandSender viewer) { @@ -90,8 +89,7 @@ public Component render(CommandSender viewer) { @Override public @NotNull RenderableComponent style(@NotNull Style style) { - this.style = style; - return this; + return new PlayerComponent(player, data, style); } @Override @@ -101,7 +99,7 @@ public Component render(CommandSender viewer) { @Override public @NotNull RenderableComponent colorIfAbsent(@Nullable TextColor color) { - if (!data.style.has(NameStyle.Flag.COLOR)) style = style.colorIfAbsent(color); - return this; + if (data.style.has(NameStyle.Flag.COLOR) || style.color() != null) return this; + return new PlayerComponent(player, data, style.colorIfAbsent(color)); } } diff --git a/core/src/main/java/tc/oc/pgm/util/player/PlayerData.java b/core/src/main/java/tc/oc/pgm/util/player/PlayerData.java index 04d8f7fd86..b91e2ba86d 100644 --- a/core/src/main/java/tc/oc/pgm/util/player/PlayerData.java +++ b/core/src/main/java/tc/oc/pgm/util/player/PlayerData.java @@ -1,5 +1,7 @@ package tc.oc.pgm.util.player; +import static tc.oc.pgm.util.player.PlayerRenderer.OFFLINE_COLOR; + import java.util.Objects; import java.util.UUID; import net.kyori.adventure.text.format.TextColor; @@ -29,7 +31,7 @@ public PlayerData(@NotNull Player player, @NotNull NameStyle style) { this.name = player.getName(); this.nick = Integration.getNick(player); MatchPlayer mp = PGM.get().getMatchManager().getPlayer(player); - this.teamColor = mp == null ? PlayerComponent.OFFLINE_COLOR : mp.getParty().getTextColor(); + this.teamColor = mp == null ? OFFLINE_COLOR : mp.getParty().getTextColor(); this.dead = mp != null && mp.isDead(); this.vanish = Integration.isVanished(player); this.online = player.isOnline(); @@ -42,8 +44,7 @@ public PlayerData(@NotNull MatchPlayer mp, @NotNull NameStyle style) { this.name = mp.getNameLegacy(); this.nick = Integration.getNick(mp.getBukkit()); - this.teamColor = - mp.getParty() == null ? PlayerComponent.OFFLINE_COLOR : mp.getParty().getTextColor(); + this.teamColor = mp.getParty() == null ? OFFLINE_COLOR : mp.getParty().getTextColor(); this.dead = mp.isDead(); this.vanish = Integration.isVanished(mp.getBukkit()); this.online = mp.getBukkit().isOnline(); @@ -71,7 +72,7 @@ public PlayerData(@Nullable Player player, @Nullable String username, @NotNull N this.nick = player != null ? Integration.getNick(player) : null; // Null-check is relevant as MatchManager will be null when loading author names. MatchPlayer mp = player != null ? PGM.get().getMatchManager().getPlayer(player) : null; - this.teamColor = mp == null ? PlayerComponent.OFFLINE_COLOR : mp.getParty().getTextColor(); + this.teamColor = mp == null ? OFFLINE_COLOR : mp.getParty().getTextColor(); this.dead = mp != null && mp.isDead(); this.vanish = mp != null && Integration.isVanished(mp.getBukkit()); this.online = player != null && player.isOnline(); diff --git a/core/src/main/java/tc/oc/pgm/util/player/PlayerRenderer.java b/core/src/main/java/tc/oc/pgm/util/player/PlayerRenderer.java index f815f253d8..ab1dadb4f4 100644 --- a/core/src/main/java/tc/oc/pgm/util/player/PlayerRenderer.java +++ b/core/src/main/java/tc/oc/pgm/util/player/PlayerRenderer.java @@ -25,7 +25,8 @@ @SuppressWarnings("UnstableApiUsage") public class PlayerRenderer { - private static final TextColor DEAD_COLOR = NamedTextColor.DARK_GRAY; + public static final TextColor DEAD_COLOR = NamedTextColor.DARK_GRAY; + public static final TextColor OFFLINE_COLOR = NamedTextColor.DARK_AQUA; private static final Style NICK_STYLE = Style.style(TextDecoration.ITALIC).decoration(TextDecoration.STRIKETHROUGH, false); @@ -44,7 +45,7 @@ public Component load(@NotNull PlayerCacheKey key) { }); } - public Component render(PlayerData data, PlayerRelationship relation) { + Component render(PlayerData data, PlayerRelationship relation) { return nameCache.getUnchecked(new PlayerCacheKey(data, relation)); } @@ -70,7 +71,7 @@ private Component render(PlayerCacheKey key) { boolean disguised = (data.nick != null || data.vanish); if (!data.online || (data.conceal && disguised && !relation.reveal)) { - return text(data.name, PlayerComponent.OFFLINE_COLOR); + return text(data.name, OFFLINE_COLOR); } String plName = relation.reveal || data.nick == null ? data.name : data.nick; diff --git a/util/src/main/java/tc/oc/pgm/util/UsernameResolver.java b/util/src/main/java/tc/oc/pgm/util/UsernameResolver.java index dcf80e41c8..88b1f69706 100644 --- a/util/src/main/java/tc/oc/pgm/util/UsernameResolver.java +++ b/util/src/main/java/tc/oc/pgm/util/UsernameResolver.java @@ -1,66 +1,29 @@ package tc.oc.pgm.util; -import static tc.oc.pgm.util.Assert.assertNotNull; - -import com.google.common.collect.ImmutableSet; -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.NoRouteToHostException; -import java.net.URL; -import java.net.UnknownHostException; -import java.nio.charset.StandardCharsets; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Semaphore; import java.util.function.Consumer; -import java.util.logging.Level; -import org.bukkit.Bukkit; -import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.Nullable; -import tc.oc.pgm.util.bukkit.BukkitUtils; +import tc.oc.pgm.util.usernames.UsernameResolver.UsernameResponse; +import tc.oc.pgm.util.usernames.UsernameResolvers; /** * Utility to resolve Minecraft usernames from an external API. * * @link https://github.com/Electroid/mojang-api + * @deprecated See {@link tc.oc.pgm.util.usernames.UsernameResolvers} instead */ +@Deprecated public final class UsernameResolver { private UsernameResolver() {} - private static final Gson GSON = new Gson(); - private static final Semaphore LOCK = new Semaphore(1); - private static final Map> QUEUE = new ConcurrentHashMap<>(); - private static final double MAX_SEQUENTIAL_FAILURES = 5; - private static String userAgent = "PGM"; - - static { - try { - final Plugin plugin = BukkitUtils.getPlugin(); - if (plugin != null) { - userAgent = plugin.getDescription().getFullName(); - } - } catch (Throwable t) { - // No-op, just to be safe in-case agent cannot be found - } - } - /** * Queue all remaining username resolves on an asynchronous thread. * * @see #resolve(UUID, Consumer) */ - public static void resolveAll() { - if (!LOCK.tryAcquire()) return; - CompletableFuture.runAsync(UsernameResolver::resolveAllSync); - } + @Deprecated + public static void resolveAll() {} /** * Queue a username resolve with an asynchronous callback. @@ -68,79 +31,9 @@ public static void resolveAll() { * @param id A {@link UUID} to resolve. * @param callback A callback to run after the username is resolved. */ + @Deprecated public static void resolve(UUID id, @Nullable Consumer callback) { - final Consumer existing = QUEUE.get(assertNotNull(id)); - if (callback == null) callback = i -> {}; - - // If a callback already exists, chain the new one after the existing one - if (existing != null && callback != existing) { - callback = existing.andThen(callback); - } - - QUEUE.put(id, callback); - } - - private static void resolveAllSync() { - LOCK.tryAcquire(); - - final Set queue = ImmutableSet.copyOf(QUEUE.keySet()); - final Map errors = new LinkedHashMap<>(); - - int fails = 0; - for (UUID id : queue) { - String name = null; - try { - name = resolveSync(id); - fails = 0; - } catch (Throwable t) { - errors.put(id, t); - if (++fails > MAX_SEQUENTIAL_FAILURES - || t instanceof UnknownHostException - || t instanceof NoRouteToHostException) break; - } finally { - final Consumer listener = QUEUE.remove(id); - if (listener != null) { - try { - listener.accept(name); - } catch (Throwable t) { - // No-op - } - } - } - } - - if (!errors.isEmpty()) { - Bukkit.getLogger() - .log( - Level.FINEST, - "Could not resolve " + errors.size() + " usernames", - errors.values().iterator().next()); - } - - LOCK.release(); - } - - private static String resolveSync(UUID id) throws IOException { - final HttpURLConnection url = - (HttpURLConnection) - new URL("https://api.ashcon.app/mojang/v2/user/" + assertNotNull(id).toString()) - .openConnection(); - url.setRequestMethod("GET"); - url.setRequestProperty("User-Agent", userAgent); - url.setRequestProperty("Accept", "application/json"); - url.setInstanceFollowRedirects(true); - url.setConnectTimeout(10000); - url.setReadTimeout(10000); - - final StringBuilder response = new StringBuilder(); - try (final BufferedReader br = - new BufferedReader(new InputStreamReader(url.getInputStream(), StandardCharsets.UTF_8))) { - String line; - while ((line = br.readLine()) != null) { - response.append(line.trim()); - } - } - - return GSON.fromJson(response.toString(), JsonObject.class).get("username").getAsString(); + CompletableFuture future = UsernameResolvers.resolve(id); + if (callback != null) future.thenAccept(ur -> callback.accept(ur.getUsername())); } } diff --git a/util/src/main/java/tc/oc/pgm/util/concurrent/ThreadSafeConnection.java b/util/src/main/java/tc/oc/pgm/util/concurrent/ThreadSafeConnection.java index c4b02e169f..5bb02f2d55 100644 --- a/util/src/main/java/tc/oc/pgm/util/concurrent/ThreadSafeConnection.java +++ b/util/src/main/java/tc/oc/pgm/util/concurrent/ThreadSafeConnection.java @@ -9,9 +9,9 @@ import java.util.LinkedList; import java.util.List; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; @@ -65,8 +65,8 @@ default void query(PreparedStatement statement) throws SQLException { * @param query A query. * @return A future when the query is complete. */ - public Future submitQuery(Query query) { - return executorService.submit( + public CompletableFuture submitQuery(Query query) { + return CompletableFuture.runAsync( () -> { try { final Connection connection = acquireConnection(); @@ -74,13 +74,16 @@ public Future submitQuery(Query query) { try (final PreparedStatement statement = connection.prepareStatement(query.getFormat())) { query.query(statement); + } catch (Throwable t) { + t.printStackTrace(); } releaseConnection(connection); } catch (SQLException e) { e.printStackTrace(); } - }); + }, + executorService); } /** diff --git a/util/src/main/java/tc/oc/pgm/util/nms/NMSHacks.java b/util/src/main/java/tc/oc/pgm/util/nms/NMSHacks.java index 2cbe12a538..c145165b22 100644 --- a/util/src/main/java/tc/oc/pgm/util/nms/NMSHacks.java +++ b/util/src/main/java/tc/oc/pgm/util/nms/NMSHacks.java @@ -283,6 +283,10 @@ static Skin getPlayerSkinForViewer(Player player, Player viewer) { return INSTANCE.getPlayerSkinForViewer(player, viewer); } + static String getPlayerName(UUID uuid) { + return INSTANCE.getPlayerName(uuid); + } + static void updateVelocity(Player player) { INSTANCE.updateVelocity(player); } diff --git a/util/src/main/java/tc/oc/pgm/util/nms/NMSHacksNoOp.java b/util/src/main/java/tc/oc/pgm/util/nms/NMSHacksNoOp.java index d8cf92f7e5..988b892cc2 100644 --- a/util/src/main/java/tc/oc/pgm/util/nms/NMSHacksNoOp.java +++ b/util/src/main/java/tc/oc/pgm/util/nms/NMSHacksNoOp.java @@ -222,6 +222,11 @@ public Skin getPlayerSkinForViewer(Player player, Player viewer) { return getPlayerSkin(player); // not possible here outside of sportpaper } + @Override + public String getPlayerName(UUID uuid) { + return Bukkit.getOfflinePlayer(uuid).getName(); + } + @Override public Set getBlockStates(Material material) { // TODO: MaterialData is not version compatible diff --git a/util/src/main/java/tc/oc/pgm/util/nms/NMSHacksPlatform.java b/util/src/main/java/tc/oc/pgm/util/nms/NMSHacksPlatform.java index 4894d88f30..056878f4fc 100644 --- a/util/src/main/java/tc/oc/pgm/util/nms/NMSHacksPlatform.java +++ b/util/src/main/java/tc/oc/pgm/util/nms/NMSHacksPlatform.java @@ -145,6 +145,8 @@ Object playerListPacketData( Skin getPlayerSkinForViewer(Player player, Player viewer); + String getPlayerName(UUID uuid); + void updateVelocity(Player player); boolean teleportRelative( diff --git a/util/src/main/java/tc/oc/pgm/util/nms/v1_8/NMSHacks1_8.java b/util/src/main/java/tc/oc/pgm/util/nms/v1_8/NMSHacks1_8.java index f5f401b629..c7199ae81d 100644 --- a/util/src/main/java/tc/oc/pgm/util/nms/v1_8/NMSHacks1_8.java +++ b/util/src/main/java/tc/oc/pgm/util/nms/v1_8/NMSHacks1_8.java @@ -659,6 +659,12 @@ public Skin getPlayerSkin(Player player) { return Skins.fromProperties(craftPlayer.getProfile().getProperties()); } + @Override + public String getPlayerName(UUID uuid) { + GameProfile profile = MinecraftServer.getServer().getUserCache().getProfile(uuid); + return profile == null ? null : profile.getName(); + } + @Override public void updateVelocity(Player player) { EntityPlayer handle = ((CraftPlayer) player).getHandle(); diff --git a/util/src/main/java/tc/oc/pgm/util/usernames/AbstractBatchingUsernameResolver.java b/util/src/main/java/tc/oc/pgm/util/usernames/AbstractBatchingUsernameResolver.java new file mode 100644 index 0000000000..0816b1bc99 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/usernames/AbstractBatchingUsernameResolver.java @@ -0,0 +1,53 @@ +package tc.oc.pgm.util.usernames; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import org.bukkit.Bukkit; + +public abstract class AbstractBatchingUsernameResolver extends AbstractUsernameResolver + implements UsernameResolver { + + protected List currentBatch = null; + + @Override + public synchronized CompletableFuture resolve(UUID uuid) { + CompletableFuture response = + futures.computeIfAbsent( + uuid, + key -> { + if (currentBatch != null) currentBatch.add(uuid); + return createFuture(uuid); + }); + + if (currentBatch == null) getExecutor().execute(() -> process(uuid, response)); + + return response; + } + + @Override + public synchronized void startBatch() { + if (currentBatch == null) currentBatch = new ArrayList<>(); + } + + @Override + public synchronized CompletableFuture endBatch() { + List batch = currentBatch; + currentBatch = null; + if (batch != null && !batch.isEmpty()) { + Bukkit.getLogger().info(LOG_PREFIX + "Batch resolving " + batch.size() + " uuids"); + + return CompletableFuture.runAsync( + () -> { + process(batch); + Bukkit.getLogger().info(LOG_PREFIX + "Done resolving " + batch.size() + " uuids"); + }, + getExecutor()); + } else { + return CompletableFuture.completedFuture(null); + } + } + + protected abstract void process(List uuids); +} diff --git a/util/src/main/java/tc/oc/pgm/util/usernames/AbstractUsernameResolver.java b/util/src/main/java/tc/oc/pgm/util/usernames/AbstractUsernameResolver.java new file mode 100644 index 0000000000..7539ad73a6 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/usernames/AbstractUsernameResolver.java @@ -0,0 +1,48 @@ +package tc.oc.pgm.util.usernames; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.ForkJoinPool; + +public abstract class AbstractUsernameResolver implements UsernameResolver { + + protected final String LOG_PREFIX = "[" + getClass().getSimpleName() + "] "; + protected final Map> futures = + new ConcurrentHashMap<>(); + + protected Executor getExecutor() { + return ForkJoinPool.commonPool(); + } + + @Override + public synchronized CompletableFuture resolve(UUID uuid) { + CompletableFuture response = + futures.computeIfAbsent(uuid, this::createFuture); + getExecutor().execute(() -> process(uuid, response)); + return response; + } + + @Override + public void startBatch() {} + + @Override + public synchronized CompletableFuture endBatch() { + return CompletableFuture.allOf(futures.values().toArray(new CompletableFuture[0])); + } + + protected CompletableFuture createFuture(UUID uuid) { + CompletableFuture future = new CompletableFuture<>(); + future.whenComplete((o, t) -> futures.remove(uuid)); + return future; + } + + protected void complete(UUID uuid, UsernameResponse response) { + CompletableFuture future = futures.get(uuid); + if (future != null) future.complete(response); + } + + protected abstract void process(UUID uuid, CompletableFuture future); +} diff --git a/util/src/main/java/tc/oc/pgm/util/usernames/ApiUsernameResolver.java b/util/src/main/java/tc/oc/pgm/util/usernames/ApiUsernameResolver.java new file mode 100644 index 0000000000..52fd67a558 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/usernames/ApiUsernameResolver.java @@ -0,0 +1,115 @@ +package tc.oc.pgm.util.usernames; + +import static tc.oc.pgm.util.Assert.assertNotNull; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.NoRouteToHostException; +import java.net.URL; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import tc.oc.pgm.util.bukkit.BukkitUtils; + +/** + * Utility to resolve Minecraft usernames from an external API. + * + * @link https://github.com/Electroid/mojang-api + */ +public final class ApiUsernameResolver extends AbstractBatchingUsernameResolver { + private static final Gson GSON = new Gson(); + private static final int MAX_SEQUENTIAL_FAILURES = 5; + private static String userAgent = "PGM"; + + static { + try { + final Plugin plugin = BukkitUtils.getPlugin(); + if (plugin != null) { + userAgent = plugin.getDescription().getFullName(); + } + } catch (Throwable t) { + // No-op, just to be safe in-case agent cannot be found + } + } + + @Override + protected void process(UUID uuid, CompletableFuture future) { + String name = null; + try { + name = resolveSync(uuid); + } catch (Throwable t) { + Bukkit.getLogger().log(Level.WARNING, "Could not resolve username for " + uuid, t); + } finally { + future.complete(UsernameResponse.of(name, ApiUsernameResolver.class)); + } + } + + @Override + protected void process(List uuids) { + final Map errors = new LinkedHashMap<>(); + + Instant now = Instant.now(); + + int fails = 0; + boolean stopped = false; + for (UUID id : uuids) { + // Even if there's an issue, we need to complete the futures. + if (stopped) { + complete(id, UsernameResponse.empty()); + continue; + } + + String name = null; + try { + name = resolveSync(id); + fails = 0; + } catch (Throwable t) { + errors.put(id, t); + if (++fails > MAX_SEQUENTIAL_FAILURES + || t instanceof UnknownHostException + || t instanceof NoRouteToHostException) { + stopped = true; + } + } finally { + complete(id, UsernameResponse.of(name, now, ApiUsernameResolver.class)); + } + } + + if (!errors.isEmpty()) { + Bukkit.getLogger() + .log( + Level.WARNING, + LOG_PREFIX + "Could not resolve " + errors.size() + " usernames", + errors.values().iterator().next()); + } + } + + private static String resolveSync(UUID id) throws IOException { + final HttpURLConnection url = + (HttpURLConnection) + new URL("https://api.ashcon.app/mojang/v2/user/" + assertNotNull(id)).openConnection(); + url.setRequestMethod("GET"); + url.setRequestProperty("User-Agent", userAgent); + url.setRequestProperty("Accept", "application/json"); + url.setInstanceFollowRedirects(true); + url.setConnectTimeout(10000); + url.setReadTimeout(10000); + + try (final BufferedReader br = + new BufferedReader(new InputStreamReader(url.getInputStream(), StandardCharsets.UTF_8))) { + return GSON.fromJson(br, JsonObject.class).get("username").getAsString(); + } + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/usernames/BukkitUsernameResolver.java b/util/src/main/java/tc/oc/pgm/util/usernames/BukkitUsernameResolver.java new file mode 100644 index 0000000000..7e5f843ad1 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/usernames/BukkitUsernameResolver.java @@ -0,0 +1,54 @@ +package tc.oc.pgm.util.usernames; + +import java.time.Instant; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; +import tc.oc.pgm.util.bukkit.BukkitUtils; +import tc.oc.pgm.util.nms.NMSHacks; + +public final class BukkitUsernameResolver extends AbstractUsernameResolver { + private static final SyncExecutor EXECUTOR = new SyncExecutor(); + private static final Plugin PGM = BukkitUtils.getPlugin(); + + @Override + protected Executor getExecutor() { + return EXECUTOR; + } + + @Override + protected void process(UUID uuid, CompletableFuture future) { + future.complete(resolveUser(uuid)); + } + + private UsernameResponse resolveUser(UUID uuid) { + OfflinePlayer pl = Bukkit.getOfflinePlayer(uuid); + if (pl.getName() != null) { + long lastPlayed = pl.getLastPlayed(); + return UsernameResponse.of( + pl.getName(), + lastPlayed > 0 ? Instant.ofEpochMilli(lastPlayed) : Instant.now(), + BukkitUsernameResolver.class); + } + + // Does vanilla user cache hold this player? + String name = NMSHacks.getPlayerName(uuid); + if (name != null) { + return UsernameResponse.of(name, Instant.now(), BukkitUsernameResolver.class); + } + return UsernameResponse.empty(); + } + + private static class SyncExecutor implements Executor { + + @Override + public void execute(@NotNull Runnable command) { + if (Bukkit.isPrimaryThread()) command.run(); + else NMSHacks.postToMainThread(PGM, false, command); + } + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/usernames/ChainedUsernameResolver.java b/util/src/main/java/tc/oc/pgm/util/usernames/ChainedUsernameResolver.java new file mode 100644 index 0000000000..dc8fd54fd5 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/usernames/ChainedUsernameResolver.java @@ -0,0 +1,81 @@ +package tc.oc.pgm.util.usernames; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public class ChainedUsernameResolver implements UsernameResolver { + + private final UsernameResolver[] resolvers; + + public ChainedUsernameResolver(UsernameResolver[] resolvers) { + this.resolvers = resolvers; + } + + @Override + public CompletableFuture resolve(UUID uuid) { + CompletableFuture future = resolvers[0].resolve(uuid); + + // Future completed sync (or very fast), avoid composing & return sync if we have a good answer + if (future.isDone() && future.getNow(UsernameResponse.empty()).isAcceptable()) { + return future; + } + + for (int i = 1; i < resolvers.length; i++) { + UsernameResolver nextResolver = resolvers[i]; + future = + future.thenCompose( + first -> { + if (first.isAcceptable()) return CompletableFuture.completedFuture(first); + return nextResolver.resolve(uuid).thenApply(second -> combine(first, second)); + }); + } + return future; + } + + @Override + public void startBatch() { + for (UsernameResolver resolver : resolvers) { + resolver.startBatch(); + } + } + + @Override + public CompletableFuture endBatch() { + CompletableFuture future = resolvers[0].endBatch(); + for (int i = 1; i < resolvers.length; i++) { + UsernameResolver resolver = resolvers[i]; + future = + future + .thenRunAsync( + () -> { + // Delay ensures composed futures had time to queue for next resolver + try { + Thread.sleep(100); + } catch (InterruptedException ignored) { + } + }) + .thenCompose(ignore -> resolver.endBatch()); + } + return future; + } + + private static UsernameResponse combine(UsernameResponse first, UsernameResponse second) { + // Operate under the assumption that first isn't acceptable; otherwise second wouldn't have been + // requested + if (second.isAcceptable()) return second; + + // Does only one have a name? + boolean firstHasName = first.getUsername() != null; + boolean secondHasName = second.getUsername() != null; + if (firstHasName != secondHasName) return firstHasName ? first : second; + // Both are bad, no name in either. + if (!firstHasName) return second; + + // Prefer comparison by validAt if available, this is a not-random value + if (first.getValidAt() != null && second.getValidAt() != null) { + return first.getValidAt().isAfter(second.getValidAt()) ? first : second; + } + // If valid at is unknown, use validUntil which has a bit of randomness, but we can take it + return first.getValidUntil().isAfter(second.getValidUntil()) ? first : second; + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/usernames/UsernameResolver.java b/util/src/main/java/tc/oc/pgm/util/usernames/UsernameResolver.java new file mode 100644 index 0000000000..41c5699852 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/usernames/UsernameResolver.java @@ -0,0 +1,82 @@ +package tc.oc.pgm.util.usernames; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public interface UsernameResolver { + + CompletableFuture resolve(UUID uuid); + + void startBatch(); + + CompletableFuture endBatch(); + + final class UsernameResponse { + private static final UsernameResponse NO_NAME = + new UsernameResponse(null, Instant.EPOCH, Instant.EPOCH, UsernameResponse.class); + + private final @Nullable String username; + private final @Nullable Instant validAt; + private final @NotNull Instant validUntil; + private final @NotNull Class source; + + private UsernameResponse( + @Nullable String username, + @Nullable Instant validAt, + @NotNull Instant validUntil, + @NotNull Class source) { + this.username = username; + this.validAt = validAt; + this.validUntil = validUntil; + this.source = source; + } + + public @Nullable String getUsername() { + return username; + } + + public @Nullable Instant getValidAt() { + return validAt; + } + + public @NotNull Instant getValidUntil() { + return validUntil; + } + + public @NotNull Class getSource() { + return source; + } + + public boolean isAcceptable() { + return username != null && validUntil.isAfter(Instant.now()); + } + + public static UsernameResponse empty() { + return NO_NAME; + } + + public static UsernameResponse of(@Nullable String name, @NotNull Class source) { + return UsernameResponse.of(name, Instant.now(), source); + } + + public static UsernameResponse of( + @Nullable String name, @NotNull Instant validAt, @NotNull Class source) { + // Assume if user was valid at time N, it will be valid until random 1 to 2 weeks after. + return UsernameResponse.of( + name, validAt, validAt.plus(7 + (long) (7 * Math.random()), ChronoUnit.DAYS), source); + } + + public static UsernameResponse of( + @Nullable String name, + @Nullable Instant validAt, + @NotNull Instant validUntil, + @NotNull Class source) { + if (name == null) return NO_NAME; + return new UsernameResponse(name, validAt, validUntil, source); + } + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/usernames/UsernameResolvers.java b/util/src/main/java/tc/oc/pgm/util/usernames/UsernameResolvers.java new file mode 100644 index 0000000000..92a1d304cd --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/usernames/UsernameResolvers.java @@ -0,0 +1,38 @@ +package tc.oc.pgm.util.usernames; + +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; + +public interface UsernameResolvers { + + AtomicReference INSTANCE = + new AtomicReference<>(of(new BukkitUsernameResolver(), new ApiUsernameResolver())); + + static UsernameResolver get() { + return INSTANCE.get(); + } + + static UsernameResolver of(UsernameResolver... resolvers) { + if (resolvers == null || resolvers.length == 0) return null; + if (resolvers.length == 1) return resolvers[0]; + return new ChainedUsernameResolver(resolvers); + } + + static void setResolvers(UsernameResolver... resolvers) { + INSTANCE.set(Objects.requireNonNull(of(resolvers))); + } + + static CompletableFuture resolve(UUID uuid) { + return INSTANCE.get().resolve(uuid); + } + + static void startBatch() { + INSTANCE.get().startBatch(); + } + + static void endBatch() { + INSTANCE.get().endBatch(); + } +}