diff --git a/.gitignore b/.gitignore index 4be6607..2f7896d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,116 +1 @@ -# User-specific stuff -.idea/ - -*.iml -*.ipr -*.iws - -# IntelliJ -out/ - -# Compiled class file -*.class - -# Log file -*.log - -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* - -*~ - -# temporary files which can be created if a process still has a handle open of a deleted file -.fuse_hidden* - -# KDE directory preferences -.directory - -# Linux trash folder which might appear on any partition or disk -.Trash-* - -# .nfs files are created when an open file is removed but is still being accessed -.nfs* - -# General -.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -# Windows thumbnail cache files -Thumbs.db -Thumbs.db:encryptable -ehthumbs.db -ehthumbs_vista.db - -# Dump file -*.stackdump - -# Folder config file -[Dd]esktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msix -*.msm -*.msp - -# Windows shortcuts -*.lnk - target/ - -pom.xml.tag -pom.xml.releaseBackup -pom.xml.versionsBackup -pom.xml.next - -release.properties -dependency-reduced-pom.xml -buildNumber.properties -.mvn/timing.properties -.mvn/wrapper/maven-wrapper.jar -.flattened-pom.xml - -# Common working directory -run/ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..ab1f416 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..58c1f83 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..eb46f0d --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + sqlite.xerial + true + org.sqlite.JDBC + jdbc:sqlite:$PROJECT_DIR$/../../../srv/smp/paper/plugins/SMPCore/smp.db + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/data_source_mapping.xml b/.idea/data_source_mapping.xml new file mode 100644 index 0000000..d67f041 --- /dev/null +++ b/.idea/data_source_mapping.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..07c9eb4 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,52 @@ + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..a2c176f --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..b83499c --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..7890b79 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml new file mode 100644 index 0000000..c5d1ee4 --- /dev/null +++ b/.idea/sqldialects.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/SMPCore.iml b/SMPCore.iml new file mode 100644 index 0000000..93a6840 --- /dev/null +++ b/SMPCore.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + PAPER + ADVENTURE + + 1 + + + + \ No newline at end of file diff --git a/src/main/java/pro/cloudnode/smp/smpcore/CachedProfile.java b/src/main/java/pro/cloudnode/smp/smpcore/CachedProfile.java new file mode 100644 index 0000000..f92b74a --- /dev/null +++ b/src/main/java/pro/cloudnode/smp/smpcore/CachedProfile.java @@ -0,0 +1,292 @@ +package pro.cloudnode.smp.smpcore; + +import com.destroystokyo.paper.profile.PlayerProfile; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import io.papermc.paper.plugin.configuration.PluginMeta; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.Duration; +import java.time.Instant; +import java.util.Date; +import java.util.Optional; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a player profile cached in the database. + * + * @param uuid UUID of the player. + * @param name Username of the player. + * @param fetched Date the profile was fetched from Mojang. + */ +@SuppressWarnings("ProfileCache") +public record CachedProfile(@NotNull UUID uuid, @NotNull String name, @NotNull Date fetched) { + private static final @NotNull Duration MAX_AGE = Duration.ofDays(7); + + static final @NotNull Duration STALE_WHILE_REVALIDATE = Duration.ofDays(1); + + private static final @NotNull HttpClient client = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(15)) + .build(); + + private static final @NotNull Logger logger = Logger + .getLogger(SMPCore.getInstance().getLogger().getName() + "/ProfileCache"); + + static { + logger.setParent(SMPCore.getInstance().getLogger()); + } + + /** + * Gets a cached profile by UUID. + * + * @param uuid UUID to look up in the database. + */ + public static @NotNull Optional<@NotNull CachedProfile> getOffline(final @NotNull UUID uuid) { + try (final PreparedStatement stmt = SMPCore.getInstance().conn.prepareStatement( + "SELECT * FROM `cached_profiles` WHERE `uuid` = ? LIMIT 1" + )) { + stmt.setString(1, uuid.toString()); + + final ResultSet rs = stmt.executeQuery(); + if (!rs.next()) + return Optional.empty(); + + return Optional.<@NotNull CachedProfile>ofNullable(CachedProfile.from(rs)); + } + catch (final SQLException e) { + logger.log(Level.SEVERE, "could not get profile for UUID " + uuid, e); + return Optional.empty(); + } + } + + /** + * Gets a cached profile for the specified OfflinePlayer. + * + * @param player OfflinePlayer to look up in the database. + */ + public static @NotNull Optional<@NotNull CachedProfile> getOffline(final @NotNull OfflinePlayer player) { + return getOffline(player.getUniqueId()); + } + + /** + * Fetches a profile from Mojang and caches it. + * + * @param uuid UUID of the player to fetch. + */ + private static @Nullable CachedProfile fetch(final @NotNull UUID uuid) { + logger.fine("Miss for UUID " + uuid); + + final PluginMeta meta = SMPCore.getInstance().getPluginMeta(); + + final HttpRequest req = HttpRequest.newBuilder() + .uri(URI.create("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid.toString())) + .header("User-Agent", meta.getName() + "/" + meta.getVersion()) + .GET() + .build(); + + try { + final HttpResponse res = client + .send(req, HttpResponse.BodyHandlers.ofString()); + + if (res.statusCode() >= 400 || res.statusCode() == 204 || res.statusCode() < 200 || res.body() == null) { + logger.log(Level.SEVERE, "got HTTP status " + res.statusCode() + + " for UUID " + uuid + ". Body is" + (res.body() == null ? "" : " not") + + " null."); + return null; + } + + final JsonObject body = JsonParser.parseString(res.body()).getAsJsonObject(); + + return new CachedProfile(uuid, body.get("name").getAsString(), new Date()) + .save(); + } + catch (final IOException | InterruptedException e) { + logger.log(Level.SEVERE, "could not fetch profile for UUID " + uuid, e); + return null; + } + } + + /** + * Fetches a profile from Mojang and caches it. + * + * @param player OfflinePlayer to fetch. + */ + private static @Nullable CachedProfile fetch(final @NotNull OfflinePlayer player) { + return fetch(player.getUniqueId()); + } + + /** + * Gets a profile from the cache or fetches it from Mojang if cache stale or missed. + * + * @param uuid UUID of the player to fetch. + * @throws IllegalStateException if the profile could not be fetched. + */ + public static @NotNull CachedProfile get(final @NotNull UUID uuid) throws IllegalStateException { + final @Nullable CachedProfile profile = getOffline(uuid).orElseGet(() -> fetch(uuid)); + + if (profile == null) + throw new IllegalStateException("profile for UUID " + uuid + " could not be fetched"); + + return profile; + } + + /** + * Gets a profile from the cache or fetches it from Mojang if cache stale or missed. + * @param player OfflinePlayer to fetch. + * @throws IllegalStateException if the profile could not be fetched. + */ + public static @NotNull CachedProfile get(final @NotNull OfflinePlayer player) throws IllegalStateException { + return get(player.getUniqueId()); + } + + /** + * Get OfflinePlayer by name if cached. + * + * @param name Username of the player to look up. + */ + public static @Nullable OfflinePlayer getOffline(final @NotNull String name) { + final @Nullable OfflinePlayer player = Bukkit.getOfflinePlayerIfCached(name); + if (player != null) + return player; + + try (final PreparedStatement stmt = SMPCore.getInstance().conn.prepareStatement( + "SELECT * from `cached_profiles` where `name` = ? LIMIT 1" + )) { + stmt.setString(1, name); + + final ResultSet rs = stmt.executeQuery(); + if (!rs.next()) + return null; + + final @Nullable CachedProfile profile = CachedProfile.from(rs); + if (profile == null) + return null; + + return Bukkit.getOfflinePlayer(profile.uuid()); + } + catch (final SQLException e) { + logger.log(Level.SEVERE, "could not get cached profile for name " + name, e); + return null; + } + } + + /** + * Get OfflinePlayer by name, fetching from Mojang if necessary. + * + * @param name Username of the player to look up. + */ + public static @NotNull OfflinePlayer get(final @NotNull String name) { + return Optional.<@NotNull OfflinePlayer>ofNullable(getOffline(name)) + .orElseGet(() -> { + logger.fine("Miss for name " + name); + return Bukkit.getOfflinePlayer(name); + }); + } + + /** + * Deletes stale cached profiles. + */ + public static void cleanUp() { + logger.fine("Cleaning up stale cached profiles..."); + try (final @NotNull PreparedStatement stmt = SMPCore.getInstance().conn.prepareStatement( + "DELETE FROM `cached_profiles` WHERE `fetched` < ?" + )) { + stmt.setTimestamp(1, new Timestamp(staleWhileRevalidateThreshold().getTime())); + stmt.execute(); + } + catch (final SQLException e) { + logger.log(Level.SEVERE, "could not clean up cached profiles", e); + } + } + + public static @NotNull String getName(final @NotNull OfflinePlayer player) { + final @Nullable String name = player.getName(); + if (name != null) + return name; + + final PlayerProfile profile = player.getPlayerProfile(); + + if (profile.completeFromCache(true)) { + final @Nullable String profileName = profile.getName(); + if (profileName != null && !profileName.isEmpty()) + return profileName; + } + + try { + return get(player).name(); + } + catch (IllegalStateException e) { + logger.log(Level.SEVERE, "Failed to fetch", e); + return player.getUniqueId().toString(); + } + } + + private static @NotNull Date expirationThreshold() { + return new Date(System.currentTimeMillis() - MAX_AGE.toMillis()); + } + + private static @NotNull Date staleWhileRevalidateThreshold() { + return new Date(System.currentTimeMillis() - STALE_WHILE_REVALIDATE.toMillis() - MAX_AGE.toMillis()); + } + + private static @Nullable CachedProfile from(final ResultSet rs) throws SQLException { + final CachedProfile profile = new CachedProfile( + UUID.fromString(rs.getString("uuid")), + rs.getString("name"), + rs.getTimestamp("fetched") + ); + + if (profile.stale()) { + logger.fine("Profile cache is stale for " + profile.uuid + " (" + + Duration.between(profile.fetched.toInstant(), Instant.now()) + " old). Revalidation scheduled."); + SMPCore.runAsync(() -> CachedProfile.fetch(profile.uuid())); + + if (profile.staleWhileRevalidate()) { + logger.fine("Serving stale cache for " + profile.uuid + " while revalidating."); + return profile; + } + return null; + } + + return profile; + } + + private boolean stale() { + return fetched.before(expirationThreshold()); + } + + private boolean staleWhileRevalidate() { + return fetched.before(staleWhileRevalidateThreshold()); + } + + private @NotNull CachedProfile save() { + SMPCore.runAsync(() -> { + try (final PreparedStatement stmt = SMPCore.getInstance().conn.prepareStatement( + "INSERT OR REPLACE INTO `cached_profiles` (`uuid`, `name`, `fetched`) VALUES (?, ?, ?)" + )) { + stmt.setString(1, uuid().toString()); + stmt.setString(2, name()); + stmt.setTimestamp(3, new Timestamp(fetched().getTime())); + stmt.executeUpdate(); + } catch (final SQLException e) { + logger.log(Level.SEVERE, "could not save profile for UUID " + uuid, e); + } + }); + + return this; + } +} diff --git a/src/main/java/pro/cloudnode/smp/smpcore/Configuration.java b/src/main/java/pro/cloudnode/smp/smpcore/Configuration.java index e85a0c8..0cb9091 100644 --- a/src/main/java/pro/cloudnode/smp/smpcore/Configuration.java +++ b/src/main/java/pro/cloudnode/smp/smpcore/Configuration.java @@ -7,7 +7,6 @@ import org.bukkit.Bukkit; import org.bukkit.OfflinePlayer; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.time.Duration; import java.time.temporal.ChronoUnit; @@ -114,9 +113,7 @@ public boolean deathBanEnabled() { } public void staffCommands(final boolean addMode, final @NotNull OfflinePlayer player) { - final @Nullable String name = player.getName(); - if (name == null) - return; + final String name = CachedProfile.getName(player); final List commands = config.getStringList("staff.commands." + (addMode ? "add" : "remove")); diff --git a/src/main/java/pro/cloudnode/smp/smpcore/Member.java b/src/main/java/pro/cloudnode/smp/smpcore/Member.java index e28b760..0ed8602 100644 --- a/src/main/java/pro/cloudnode/smp/smpcore/Member.java +++ b/src/main/java/pro/cloudnode/smp/smpcore/Member.java @@ -13,7 +13,6 @@ import java.sql.SQLException; import java.util.Date; import java.util.HashSet; -import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -82,7 +81,8 @@ public boolean isAlt() { while (rs.next()) alts.add(new Member(rs)); } catch (final @NotNull SQLException e) { - SMPCore.getInstance().getLogger().log(Level.SEVERE, "could not get alts for " + player().getName(), e); + SMPCore.getInstance().getLogger().log(Level.SEVERE, "could not get alts for " + + CachedProfile.getName(player()), e); } return alts; @@ -248,14 +248,17 @@ public static int count() { } } - @SuppressWarnings("NullableProblems") public static @NotNull Set<@NotNull String> getNames() { - return get().stream().map(m -> m.player().getName()).filter(Objects::nonNull).collect(Collectors.toSet()); + return get().stream() + .map(m -> CachedProfile.getName(m.player())) + .collect(Collectors.toSet()); } - @SuppressWarnings("NullableProblems") public static @NotNull Set<@NotNull String> getAltNames() { - return get().stream().filter(Member::isAlt).map(m -> m.player().getName()).filter(Objects::nonNull).collect(Collectors.toSet()); + return get().stream() + .filter(Member::isAlt) + .map(m -> CachedProfile.getName(m.player())) + .collect(Collectors.toSet()); } public static @NotNull Team createStaffTeam() { diff --git a/src/main/java/pro/cloudnode/smp/smpcore/Messages.java b/src/main/java/pro/cloudnode/smp/smpcore/Messages.java index c94aeec..4cd3357 100644 --- a/src/main/java/pro/cloudnode/smp/smpcore/Messages.java +++ b/src/main/java/pro/cloudnode/smp/smpcore/Messages.java @@ -36,12 +36,17 @@ public Messages() { } public @NotNull Component reloaded() { - return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("reloaded"))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("reloaded")) + ); } public @NotNull Component usage(final @NotNull String label, final @NotNull String args) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("usage")), Placeholder.unparsed("label", label), Placeholder.unparsed("args", args)); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("usage")), + Placeholder.unparsed("label", label), + Placeholder.unparsed("args", args) + ); } private @NotNull Component formatDuration(@Nullable Duration duration) { @@ -66,25 +71,18 @@ public Messages() { } public @NotNull Component bannedPlayer(final @NotNull OfflinePlayer player, final @Nullable Duration duration) { - return MiniMessage.miniMessage() - .deserialize( - Objects.requireNonNull(config.getString("banned-player")), - Placeholder.unparsed("player", - Optional.ofNullable(player.getName()) - .orElse(player.getUniqueId().toString()) - ), - Placeholder.component("duration", formatDuration(duration)) - ); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("banned-player")), + Placeholder.unparsed("player", CachedProfile.getName(player)), + Placeholder.component("duration", formatDuration(duration)) + ); } public @NotNull Component bannedMember(final @NotNull Member member, final @Nullable Duration duration) { return MiniMessage.miniMessage() .deserialize( Objects.requireNonNull(config.getString("banned-member")), - Placeholder.unparsed("player", - Optional.ofNullable(member.player().getName()) - .orElse(member.player().getUniqueId().toString()) - ), + Placeholder.unparsed("player", CachedProfile.getName(member.player())), Placeholder.component("duration", formatDuration(duration)) ); } @@ -94,16 +92,14 @@ public Messages() { final @NotNull List<@NotNull Member> alts, final @Nullable Duration duration ) { - final @NotNull String altsString = alts.stream() - .map(m -> Optional.ofNullable(m.player().getName()).orElse(m.player().getUniqueId().toString())) + final String altsString = alts.stream() + .map(m -> CachedProfile.getName(m.player())) .collect(Collectors.joining(", ")); + return MiniMessage.miniMessage() .deserialize( Objects.requireNonNull(config.getString("banned-member-chain")), - Placeholder.unparsed("player", - Optional.ofNullable(member.player().getName()) - .orElse(member.player().getUniqueId().toString()) - ), + Placeholder.unparsed("player", CachedProfile.getName(member.player())), Placeholder.unparsed("n-alt", String.valueOf(alts.size())), Placeholder.unparsed("alts", altsString), Placeholder.component("duration", formatDuration(duration)) @@ -111,92 +107,135 @@ public Messages() { } public @NotNull Component unbannedPlayer(final @NotNull OfflinePlayer player) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("unbanned-player")), Placeholder.unparsed("player", Optional - .ofNullable(player.getName()).orElse(player.getUniqueId().toString()))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("unbanned-player")), + Placeholder.unparsed("player", CachedProfile.getName(player)) + ); } public @NotNull Component unbannedMember(final @NotNull Member member, final @NotNull List<@NotNull Member> alts) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("unbanned-member")), Placeholder.unparsed("player", Optional - .ofNullable(member.player().getName()).orElse(member.player().getUniqueId() - .toString())), Placeholder.unparsed("n-alts", String.valueOf(alts.size()))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("unbanned-member")), + Placeholder.unparsed("player", CachedProfile.getName(member.player())), + Placeholder.unparsed("n-alts", String.valueOf(alts.size())) + ); } // subcommands public @NotNull Component subCommandHeader(final @NotNull String name, final @NotNull String usage) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("subcommands.header")), Placeholder.unparsed("name", name), Placeholder.unparsed("usage", usage)); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("subcommands.header")), + Placeholder.unparsed("name", name), + Placeholder.unparsed("usage", usage) + ); } - public @NotNull Component subCommandEntry(final @NotNull String command, final @NotNull String label, final @NotNull SubCommandArgument @NotNull [] args) { - return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("subcommands.entry")) - .replace("", command), Placeholder.unparsed("label", label), Placeholder.component("args", SubCommandArgument.join(args))); + public @NotNull Component subCommandEntry( + final @NotNull String command, + final @NotNull String label, + final @NotNull SubCommandArgument @NotNull [] args + ) { + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("subcommands.entry")).replace("", command), + Placeholder.unparsed("label", label), + Placeholder.component("args", SubCommandArgument.join(args)) + ); } public @NotNull Component subCommandEntry(final @NotNull String command, final @NotNull String label) { - return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("subcommands.entry")) - .replace("", command), Placeholder.unparsed("label", label), Placeholder.component("args", SubCommandArgument.join(SubCommandArgument.of()))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("subcommands.entry")).replace("", command), + Placeholder.unparsed("label", label), + Placeholder.component("args", SubCommandArgument.join(SubCommandArgument.of())) + ); } - public @NotNull Component subCommandEntry(final @NotNull String command, final @NotNull String label, final @NotNull SubCommandArgument @NotNull [] args, final @NotNull String description) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("subcommands.entry-with-description")) - .replace("", command), Placeholder.unparsed("label", label), Placeholder.component("args", SubCommandArgument.join(args)), Placeholder.unparsed("description", description)); + public @NotNull Component subCommandEntry( + final @NotNull String command, + final @NotNull String label, + final @NotNull SubCommandArgument @NotNull [] args, + final @NotNull String description + ) { + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("subcommands.entry-with-description")) + .replace("", command), + Placeholder.unparsed("label", label), + Placeholder.component("args", SubCommandArgument.join(args)), + Placeholder.unparsed("description", description) + ); } - public @NotNull Component subCommandEntry(final @NotNull String command, final @NotNull String label, final @NotNull String description) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("subcommands.entry-with-description")) - .replace("", command), Placeholder.unparsed("label", label), Placeholder.component("args", SubCommandArgument.join(SubCommandArgument.of())), Placeholder.unparsed("description", description)); + public @NotNull Component subCommandEntry( + final @NotNull String command, + final @NotNull String label, + final @NotNull String description + ) { + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("subcommands.entry-with-description")) + .replace("", command), + Placeholder.unparsed("label", label), + Placeholder.component("args", SubCommandArgument.join(SubCommandArgument.of())), + Placeholder.unparsed("description", description) + ); } public @NotNull Component subCommandArgumentRequired(final @NotNull String argument) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("subcommands.argument.required")), Placeholder.unparsed("arg", argument)); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("subcommands.argument.required")), + Placeholder.unparsed("arg", argument) + ); } public @NotNull Component subCommandArgumentOptional(final @NotNull String argument) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("subcommands.argument.optional")), Placeholder.unparsed("arg", argument)); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("subcommands.argument.optional")), + Placeholder.unparsed("arg", argument) + ); } // end of subcommands public @NotNull Component altsListHeader(final @NotNull Member member) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("alts.list.header")), Placeholder.unparsed("player", Optional - .ofNullable(member.player().getName()).orElse(member.player().getUniqueId().toString()))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("alts.list.header")), + Placeholder.unparsed("player", CachedProfile.getName(member.player())) + ); } public @NotNull Component altsListNone() { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("alts.list.none"))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("alts.list.none")) + ); } public @NotNull Component altsListEntry(final @NotNull Member alt) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("alts.list.entry")), Placeholder.unparsed("alt", Optional - .ofNullable(alt.player().getName()).orElse(alt.player().getUniqueId().toString()))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("alts.list.entry")), + Placeholder.unparsed("alt", CachedProfile.getName(alt.player())) + ); } public @NotNull Component altsConfirmAdd(final @NotNull OfflinePlayer alt, final @NotNull String confirmCommand) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("alts.confirm-add")).replace("", confirmCommand), Placeholder.unparsed("alt", Optional - .ofNullable(alt.getName()).orElse(alt.getUniqueId().toString()))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("alts.confirm-add")) + .replace("", confirmCommand), + Placeholder.unparsed("alt", CachedProfile.getName(alt)) + ); } public @NotNull Component altsCreated(final @NotNull Member alt) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("alts.created")), Placeholder.unparsed("alt", Optional - .ofNullable(alt.player().getName()).orElse(alt.player().getUniqueId().toString()))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("alts.created")), + Placeholder.unparsed("alt", CachedProfile.getName(alt.player())) + ); } public @NotNull Component altsDeleted(final @NotNull Member alt) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("alts.deleted")), Placeholder.unparsed("alt", Optional - .ofNullable(alt.player().getName()).orElse(alt.player().getUniqueId().toString()))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("alts.deleted")), + Placeholder.unparsed("alt", CachedProfile.getName(alt.player())) + ); } public @NotNull Component membersNationlessFallback() { @@ -220,8 +259,7 @@ public Messages() { public @NotNull Component membersListEntry(final @NotNull Member member) { return MiniMessage.miniMessage().deserialize( Objects.requireNonNull(config.getString("members.list.entry")), - Placeholder.unparsed("player", Optional.ofNullable(member.player().getName()) - .orElse(member.player().getUniqueId().toString())), + Placeholder.unparsed("player", CachedProfile.getName(member.player())), Formatter.choice("staff", member.staff ? 1 : 0), Placeholder.component("nation", member.nation() .map(n -> n.getTeam().displayName()) @@ -234,463 +272,593 @@ public Messages() { public @NotNull Component membersAdded(final @NotNull Member member) { return MiniMessage.miniMessage().deserialize( Objects.requireNonNull(config.getString("members.added")), - Placeholder.unparsed("player", Optional.ofNullable(member.player().getName()) - .orElse(member.player().getUniqueId().toString())) + Placeholder.unparsed("player", CachedProfile.getName(member.player())) ); } public @NotNull Component membersDeleted(final @NotNull OfflinePlayer player) { return MiniMessage.miniMessage().deserialize( Objects.requireNonNull(config.getString("members.deleted")), - Placeholder.unparsed("player", Optional.ofNullable(player.getName()) - .orElse(player.getUniqueId().toString())) + Placeholder.unparsed("player", CachedProfile.getName(player)) ); } public @NotNull Component membersSetStaff(final @NotNull Member member) { return MiniMessage.miniMessage().deserialize( Objects.requireNonNull(config.getString("members.set-staff")), - Placeholder.unparsed("player", Optional.ofNullable(member.player().getName()) - .orElse(member.player().getUniqueId().toString())), + Placeholder.unparsed("player", CachedProfile.getName(member.player())), Formatter.choice("staff", member.staff ? 1 : 0) ); } public @NotNull Component seen(final @NotNull Member member) { - if (member.player().isOnline()) return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("seen.online")), Placeholder.unparsed("player", Optional - .ofNullable(member.player().getName()).orElse(member.uuid.toString()))); - final @NotNull Date lastSeen = new Date(member.player().getLastSeen()); - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString(member.isActive() ? "seen.active" : "seen.inactive")), Placeholder.unparsed("player", Optional - .ofNullable(member.player().getName()) - .orElse(member.uuid.toString())), Formatter.date("last-seen", lastSeen.toInstant() - .atZone(ZoneOffset.UTC) - .toLocalDateTime()), Placeholder.component("last-seen-relative", SMPCore.relativeTime(lastSeen))); + if (member.player().isOnline()) return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("seen.online")), + Placeholder.unparsed("player", CachedProfile.getName(member.player())) + ); + + final Date lastSeen = new Date(member.player().getLastSeen()); + + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString(member.isActive() ? "seen.active" : "seen.inactive")), + Placeholder.unparsed("player", CachedProfile.getName(member.player())), + Formatter.date("last-seen", lastSeen.toInstant().atZone(ZoneOffset.UTC).toLocalDateTime()), + Placeholder.component("last-seen-relative", SMPCore.relativeTime(lastSeen)) + ); } public @NotNull Component seen(final @NotNull OfflinePlayer player) { - if (player.isOnline()) return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("seen.online")), Placeholder.unparsed("player", Optional - .ofNullable(player.getName()).orElse(player.getUniqueId().toString()))); - final @NotNull Date lastSeen = new Date(player.getLastSeen()); - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("seen.non-member")), Placeholder.unparsed("player", Optional - .ofNullable(player.getName()) - .orElse(player.getUniqueId().toString())), Formatter.date("last-seen", lastSeen.toInstant() - .atZone(ZoneOffset.UTC) - .toLocalDateTime()), Placeholder.component("last-seen-relative", SMPCore.relativeTime(lastSeen))); + if (player.isOnline()) + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("seen.online")), + Placeholder.unparsed("player", CachedProfile.getName(player)) + ); + + final Date lastSeen = new Date(player.getLastSeen()); + + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("seen.non-member")), + Placeholder.unparsed("player", CachedProfile.getName(player)), + Formatter.date("last-seen", lastSeen.toInstant().atZone(ZoneOffset.UTC).toLocalDateTime()), + Placeholder.component("last-seen-relative", SMPCore.relativeTime(lastSeen)) + ); } public @NotNull Component time(final @NotNull Date date) { - final @NotNull Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + final Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); calendar.setTime(date); + final int day = calendar.get(Calendar.DAY_OF_MONTH); - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("time")), Formatter.date("date", calendar - .toInstant().atZone(ZoneOffset.UTC) - .toLocalDateTime()), Placeholder.unparsed("day", String.valueOf(day)), Placeholder.unparsed("day", String.valueOf(day)), Formatter.choice("day-format", day)); + + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("time")), + Formatter.date("date", calendar.toInstant().atZone(ZoneOffset.UTC).toLocalDateTime()), + Placeholder.unparsed("day", String.valueOf(day)), + Placeholder.unparsed("day", String.valueOf(day)), + Formatter.choice("day-format", day) + ); } public @NotNull Component nationCitizensStatus(final @NotNull Member member) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString(member.isActive() ? "nation.citizens.status.active" : "nation.citizens.status.inactive"))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString(member.isActive() + ? "nation.citizens.status.active" + : "nation.citizens.status.inactive")) + ); } - public @NotNull Component nationCitizensList(final @NotNull Nation nation, final @NotNull Permissible sender, final boolean other) { - final @NotNull Set<@NotNull Member> members = nation.citizens(); - final @NotNull Component header = MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("nation.citizens.list.header")) + public @NotNull Component nationCitizensList( + final @NotNull Nation nation, + final @NotNull Permissible sender, + final boolean other + ) { + final Set members = nation.citizens(); + + final Component header = MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("nation.citizens.list.header")) .replaceAll("", "<#" + nation.color + ">") .replaceAll("", ""), - Placeholder.unparsed("nation-name", nation.name), - Placeholder.unparsed("nation-active-members", members.stream().filter(Member::isActive).count() + ""), - Placeholder.unparsed("nation-inactive-members", members.stream().filter(m -> !m.isActive()).count() + ""), - Placeholder.unparsed("nation-total-members", members.size() + "") - ); + Placeholder.unparsed("nation-name", nation.name), + Placeholder.unparsed("nation-active-members", String.valueOf(members.stream() + .filter(Member::isActive) + .count() + )), + Placeholder.unparsed("nation-inactive-members", String.valueOf(members.stream() + .filter(m -> !m.isActive()) + .count() + )), + Placeholder.unparsed("nation-total-members", String.valueOf(members.size())) + ); + + final List list = new ArrayList<>(); - final @NotNull List<@NotNull Component> list = new ArrayList<>(); + final Member leader = members.stream() + .filter(m -> m.uuid.equals(nation.leaderUUID)) + .findFirst() + .orElseThrow(IllegalStateException::new); - final @NotNull Member leader = members.stream().filter(m -> m.uuid.equals(nation.leaderUUID)).findFirst().orElseThrow(IllegalStateException::new); list.add(nationCitizensListEntry(nation, leader, sender, other)); - final @NotNull Member vice = members.stream().filter(m -> m.uuid.equals(nation.viceLeaderUUID)).findFirst().orElseThrow(IllegalStateException::new); + final Member vice = members.stream() + .filter(m -> m.uuid.equals(nation.viceLeaderUUID)) + .findFirst() + .orElseThrow(IllegalStateException::new); + if (!vice.uuid.equals(leader.uuid)) list.add(nationCitizensListEntry(nation, vice, sender, other)); - final @NotNull List<@NotNull Component> citizens = members.stream().filter(m -> !m.uuid.equals(leader.uuid) && !m.uuid.equals(vice.uuid) && m.isActive()).map(m -> nationCitizensListEntry(nation, m, sender, other)).toList(); + final List citizens = members.stream() + .filter(m -> !m.uuid.equals(leader.uuid) && !m.uuid.equals(vice.uuid) && m.isActive()) + .map(m -> nationCitizensListEntry(nation, m, sender, other)) + .toList(); + list.addAll(citizens); - final @NotNull List<@NotNull Component> inactive = members.stream().filter(m -> !m.uuid.equals(leader.uuid) && !m.uuid.equals(vice.uuid) && !m.isActive()).map(m -> nationCitizensListEntry(nation, m, sender, other)).toList(); + final List inactive = members.stream() + .filter(m -> !m.uuid.equals(leader.uuid) && !m.uuid.equals(vice.uuid) && !m.isActive()) + .map(m -> nationCitizensListEntry(nation, m, sender, other)) + .toList(); + list.addAll(inactive); - final @NotNull TextComponent.Builder listComponent = Component.text(); - for (int i = 0; i < list.size(); i++) { + final TextComponent.Builder listComponent = Component.text(); + + for (int i = 0; i < list.size(); ++i) { listComponent.append(list.get(i)); - if (i + 1 < list.size()) listComponent.append(Component.newline()); + + if (i + 1 < list.size()) + listComponent.append(Component.newline()); } - return Component.text().append(header).append(Component.newline()).append(listComponent.build()).build(); + return Component.text() + .append(header) + .append(Component.newline()) + .append(listComponent.build()) + .build(); } - private @NotNull Component nationCitizensListEntry(final @NotNull Nation nation, final @NotNull Member member, final @NotNull Permissible sender, final boolean other) { - if (member.uuid.equals(nation.leaderUUID)) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("nation.citizens.list.entry.leader")) - .replaceAll("", "<#" + nation.color + ">") - .replaceAll("", "") - .replaceAll("", Optional.ofNullable(member.player().getName()).orElse(member.uuid.toString())), - Placeholder.component("member-status", nationCitizensStatus(member)), - Placeholder.component("buttons", nationCitizensListButtons(nation, member, sender, other)) - ); - } - if (member.uuid.equals(nation.viceLeaderUUID)) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("nation.citizens.list.entry.vice")) - .replaceAll("", "<#" + nation.color + ">") - .replaceAll("", "") - .replaceAll("", Optional.ofNullable(member.player().getName()).orElse(member.uuid.toString())), - Placeholder.component("member-status", nationCitizensStatus(member)), - Placeholder.component("buttons", nationCitizensListButtons(nation, member, sender, other)) - ); - } - if (member.isActive()) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("nation.citizens.list.entry.citizen")) - .replaceAll("", "<#" + nation.color + ">") - .replaceAll("", "") - .replaceAll("", Optional.ofNullable(member.player().getName()).orElse(member.uuid.toString())), - Placeholder.component("member-status", nationCitizensStatus(member)), - Placeholder.component("buttons", nationCitizensListButtons(nation, member, sender, other)) - ); - } - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("nation.citizens.list.entry.inactive-citizen")) - .replaceAll("", "<#" + nation.color + ">") - .replaceAll("", "") - .replaceAll("", Optional.ofNullable(member.player().getName()).orElse(member.uuid.toString())), - Placeholder.component("member-status", nationCitizensStatus(member)), - Placeholder.component("buttons", nationCitizensListButtons(nation, member, sender, other)) - ); + private @NotNull Component nationCitizensListEntry( + final @NotNull Nation nation, + final @NotNull Member member, + final @NotNull Permissible sender, + final boolean other + ) { + final String configKey; + + if (member.uuid.equals(nation.leaderUUID)) + configKey = "nation.citizens.list.entry.leader"; + else if (member.uuid.equals(nation.viceLeaderUUID)) + configKey = "nation.citizens.list.entry.vice"; + else if (member.isActive()) + configKey = "nation.citizens.list.entry.citizen"; + else + configKey = "nation.citizens.list.entry.inactive-citizen"; + + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString(configKey)) + .replaceAll("", "<#" + nation.color + ">") + .replaceAll("", "") + .replaceAll("", CachedProfile.getName(member.player())), + Placeholder.component("member-status", nationCitizensStatus(member)), + Placeholder.component("buttons", nationCitizensListButtons(nation, member, sender, other)) + ); } - private @NotNull Component nationCitizensListButtons(final @NotNull Nation nation, final @NotNull Member member, final @NotNull Permissible sender, final boolean other) { - final @NotNull List<@NotNull Component> buttons = new ArrayList<>(); - if ( - ((!other && sender.hasPermission(Permission.NATION_CITIZENS_KICK)) - || sender.hasPermission(Permission.NATION_CITIZENS_KICK_OTHER)) - && !(member.uuid.equals(nation.leaderUUID) || member.uuid.equals(nation.viceLeaderUUID)) - ) + private @NotNull Component nationCitizensListButtons( + final @NotNull Nation nation, + final @NotNull Member member, + final @NotNull Permissible sender, + final boolean other + ) { + final List buttons = new ArrayList<>(); + + final boolean canKick = (!other && sender.hasPermission(Permission.NATION_CITIZENS_KICK)) || + sender.hasPermission(Permission.NATION_CITIZENS_KICK_OTHER); + final boolean canDemote = (!other && sender.hasPermission(Permission.NATION_DEMOTE)) || + sender.hasPermission(Permission.NATION_DEMOTE_OTHER); + final boolean canPromote = (!other && sender.hasPermission(Permission.NATION_PROMOTE)) || + sender.hasPermission(Permission.NATION_PROMOTE_OTHER); + + final boolean isLeader = member.uuid.equals(nation.leaderUUID); + final boolean isVice = member.uuid.equals(nation.viceLeaderUUID); + final boolean hasVice = !nation.viceLeaderUUID.equals(nation.leaderUUID); + + if (canKick && !isLeader && !isVice) buttons.add(nationCitizensListButton("kick", nation, member)); - if ( - ((!other && sender.hasPermission(Permission.NATION_DEMOTE)) - || sender.hasPermission(Permission.NATION_DEMOTE_OTHER)) - && member.uuid.equals(nation.viceLeaderUUID) - ) + + if (canDemote && isVice) buttons.add(nationCitizensListButton("demote", nation, member)); - if ( - ((!other && sender.hasPermission(Permission.NATION_PROMOTE)) - || sender.hasPermission(Permission.NATION_PROMOTE_OTHER)) - && nation.viceLeaderUUID.equals(nation.leaderUUID) - && !member.uuid.equals(nation.leaderUUID) - && member.isActive() - ) + if (canPromote && !hasVice && !isLeader && member.isActive()) buttons.add(nationCitizensListButton("promote", nation, member)); - final @NotNull TextComponent.Builder buttonsComponent = Component.text(); - if (!buttons.isEmpty()) buttonsComponent.append(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("nation.citizens.list.buttons.prefix")))); - for (int i = 0; i < buttons.size(); i++) { + final TextComponent.Builder buttonsComponent = Component.text(); + + if (!buttons.isEmpty()) buttonsComponent.append(MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("nation.citizens.list.buttons.prefix")) + )); + + for (int i = 0; i < buttons.size(); ++i) { buttonsComponent.append(buttons.get(i)); - if (i + 1 < buttons.size()) buttonsComponent.append(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("nation.citizens.list.buttons.separator")))); + + if (i + 1 < buttons.size()) + buttonsComponent.append(MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("nation.citizens.list.buttons.separator")) + )); } - if (!buttons.isEmpty()) buttonsComponent.append(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("nation.citizens.list.buttons.suffix")))); - return Component.text().append(buttonsComponent.build()).build(); + if (!buttons.isEmpty()) + buttonsComponent.append(MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("nation.citizens.list.buttons.suffix")) + )); + + return Component.text() + .append(buttonsComponent.build()) + .build(); } - private @NotNull Component nationCitizensListButton(final @NotNull String button, final @NotNull Nation nation, final @NotNull Member member) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("nation.citizens.list.buttons." + button)) - .replaceAll("", "<#" + nation.color + ">") - .replaceAll("", "") - .replaceAll("", Optional.ofNullable(member.player().getName()).orElse(member.uuid.toString()))); + private @NotNull Component nationCitizensListButton( + final @NotNull String button, + final @NotNull Nation nation, + final @NotNull Member member + ) { + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("nation.citizens.list.buttons." + button)) + .replaceAll("", "<#" + nation.color + ">") + .replaceAll("", "") + .replaceAll("", CachedProfile.getName(member.player())) + ); } public @NotNull Component nationCitizensKicked(final @NotNull Member member) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("nation.citizens.kicked")), Placeholder.unparsed("player", Optional - .ofNullable(member.player().getName()).orElse(member.uuid.toString()))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("nation.citizens.kicked")), + Placeholder.unparsed("player", CachedProfile.getName(member.player())) + ); } public @NotNull Component nationCitizensVicePromoted(final @NotNull Member member) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("nation.citizens.vice.promoted")), Placeholder.unparsed("player", Optional.ofNullable(member.player().getName()).orElse(member.uuid.toString()))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("nation.citizens.vice.promoted")), + Placeholder.unparsed("player", CachedProfile.getName(member.player())) + ); } public @NotNull Component nationCitizensVicePromoted() { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("nation.citizens.vice.promoted-you"))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("nation.citizens.vice.promoted-you")) + ); } public @NotNull Component nationCitizensViceDemoted(final @NotNull Member member) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("nation.citizens.vice.demoted")), Placeholder.unparsed("player", Optional.ofNullable(member.player().getName()).orElse(member.uuid.toString()))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("nation.citizens.vice.demoted")), + Placeholder.unparsed("player", CachedProfile.getName(member.player())) + ); } public @NotNull Component nationCitizensViceDemoted() { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("nation.citizens.vice.demoted-you"))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("nation.citizens.vice.demoted-you")) + ); } public @NotNull Component nationJoinRequestSent(final @NotNull Nation nation) { - return MiniMessage.miniMessage() - .deserialize( - Objects.requireNonNull(config.getString("nation.join.request-sent")) - .replaceAll("", nation.id), - Placeholder.unparsed("nation", nation.name) - ); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("nation.join.request-sent")) + .replaceAll("", nation.id), + Placeholder.unparsed("nation", nation.name) + ); } public @NotNull Component nationJoinRequestReceived(final @NotNull Member member) { - return MiniMessage.miniMessage() - .deserialize( - Objects.requireNonNull(config.getString("nation.join.request-received")) - .replaceAll("", Optional.ofNullable(member.player().getName()).orElse(member.uuid.toString())) - ); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("nation.join.request-received")) + .replaceAll("", CachedProfile.getName(member.player())) + ); } public @NotNull Component nationJoinInviteSent(final @NotNull Member member) { - return MiniMessage.miniMessage() - .deserialize( - Objects.requireNonNull(config.getString("nation.join.invite-sent")) - .replaceAll("", Optional.ofNullable(member.player().getName()).orElse(member.uuid.toString())) - ); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("nation.join.invite-sent")) + .replaceAll("", CachedProfile.getName(member.player())) + ); } public @NotNull Component nationJoinInviteReceived(final @NotNull Nation nation) { - return MiniMessage.miniMessage() - .deserialize( - Objects.requireNonNull(config.getString("nation.join.invite-received")) - .replaceAll("", nation.id), - Placeholder.unparsed("nation", nation.name) - ); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("nation.join.invite-received")) + .replaceAll("", nation.id), + Placeholder.unparsed("nation", nation.name) + ); } public @NotNull Component nationJoinJoined(final @NotNull Member member) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("nation.join.joined")), Placeholder.unparsed("player", Optional - .ofNullable(member.player().getName()).orElse(member.uuid.toString()))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("nation.join.joined")), + Placeholder.unparsed("player", CachedProfile.getName(member.player())) + ); } public @NotNull Component nationJoinLeft(final @NotNull Member member) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("nation.join.left")), Placeholder.unparsed("player", Optional - .ofNullable(member.player().getName()).orElse(member.uuid.toString()))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("nation.join.left")), + Placeholder.unparsed("player", CachedProfile.getName(member.player())) + ); } public @NotNull Component nationJoinRequestCancelled(final @NotNull Nation nation) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("nation.join.request-cancelled")), Placeholder.unparsed("nation", nation.name)); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("nation.join.request-cancelled")), + Placeholder.unparsed("nation", nation.name) + ); } public @NotNull Component nationJoinRequestRejected(final @NotNull Member member, final @NotNull Nation nation) { - return MiniMessage.miniMessage() - .deserialize( - Objects.requireNonNull(config.getString("nation.join.request-rejected")), - Placeholder.unparsed("player", Optional.ofNullable( - member.player().getName()).orElse(member.uuid.toString()) - ), - Placeholder.unparsed("nation", nation.name) - ); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("nation.join.request-rejected")), + Placeholder.unparsed("player", CachedProfile.getName(member.player())), + Placeholder.unparsed("nation", nation.name) + ); } public @NotNull Component nationJoinInviteCancelled(final @NotNull Member member, final @NotNull Nation nation) { - return MiniMessage.miniMessage() - .deserialize( - Objects.requireNonNull(config.getString("nation.join.invite-cancelled")), - Placeholder.unparsed("player", Optional.ofNullable( - member.player().getName()).orElse(member.uuid.toString()) - ), - Placeholder.unparsed("nation", nation.name) - ); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("nation.join.invite-cancelled")), + Placeholder.unparsed("player", CachedProfile.getName(member.player())), + Placeholder.unparsed("nation", nation.name) + ); } public @NotNull Component nationJoinInviteRejected(final @NotNull Nation nation) { - return MiniMessage.miniMessage() - .deserialize( - Objects.requireNonNull(config.getString("nation.join.invite-rejected")), - Placeholder.unparsed("nation", nation.name) - ); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("nation.join.invite-rejected")), + Placeholder.unparsed("nation", nation.name) + ); } public @NotNull Optional<@NotNull Component> banScreen(final @NotNull BanEntry banEntry) { final @Nullable Date expiration = banEntry.getExpiration(); - final @Nullable String template = config.getString("ban-screen." + (expiration == null ? "permanent" : "temporary")); + + final @Nullable String template = config.getString("ban-screen." + (expiration == null + ? "permanent" + : "temporary" + )); + if (template == null || template.isBlank() || template.equals("null")) return Optional.empty(); - List placeholders = new ArrayList<>(); + final List placeholders = new ArrayList<>(); + placeholders.add(Placeholder.unparsed("reason", Optional.ofNullable(banEntry.getReason()).orElse(""))); + if (expiration != null) { - final @NotNull ZonedDateTime localExpiry = expiration.toInstant().atZone(ZoneOffset.systemDefault()); + final ZonedDateTime localExpiry = expiration.toInstant().atZone(ZoneOffset.systemDefault()); + placeholders.add(Formatter.date("expiration", localExpiry)); placeholders.add(Placeholder.component("expiration-relative", SMPCore.relativeTime(expiration))); } - return Optional.of(MiniMessage.miniMessage().deserialize(template, placeholders.toArray(TagResolver[]::new))); + return Optional.of(MiniMessage.miniMessage().deserialize( + template, + placeholders.toArray(TagResolver[]::new) + )); } // errors public @NotNull Component errorNoPermission() { - return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("error.no-permission"))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.no-permission")) + ); } public @NotNull Component errorPlayerNotBanned(final @NotNull OfflinePlayer player) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("error.player-not-banned")), Placeholder.unparsed("player", Optional - .ofNullable(player.getName()).orElse(player.getUniqueId().toString()))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.player-not-banned")), + Placeholder.unparsed("player", CachedProfile.getName(player)) + ); } public @NotNull Component errorNotMember(final @NotNull OfflinePlayer player) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("error.not-member")), Placeholder.unparsed("player", Optional - .ofNullable(player.getName()).orElse(player.getUniqueId().toString()))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.not-member")), + Placeholder.unparsed("player", CachedProfile.getName(player)) + ); } public @NotNull Component errorNotMember() { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("error.not-member-you"))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.not-member-you")) + ); } - public @NotNull Component errorAltAlreadyMember(final @NotNull Member player) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("error.alt-already-member")), Placeholder.unparsed("player", Optional - .ofNullable(player.player().getName()).orElse(player.player().getUniqueId().toString()))); + public @NotNull Component errorAltAlreadyMember(final @NotNull Member member) { + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.alt-already-member")), + Placeholder.unparsed("player", CachedProfile.getName(member.player())) + ); } public @NotNull Component errorDisallowedCharacters(final @NotNull Set<@NotNull Character> chars) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("error.disallowed-characters")), Placeholder.unparsed("chars", chars.stream().map(String::valueOf).collect(Collectors.joining()))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.disallowed-characters")), + Placeholder.unparsed("chars", chars.stream().map(String::valueOf).collect(Collectors.joining())) + ); } - public @NotNull Component errorFailedDeleteMember(final @NotNull Member player) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("error.failed-delete-member")), Placeholder.unparsed("player", Optional - .ofNullable(player.player().getName()).orElse(player.player().getUniqueId().toString()))); + public @NotNull Component errorFailedDeleteMember(final @NotNull Member member) { + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.failed-delete-member")), + Placeholder.unparsed("player", CachedProfile.getName(member.player())) + ); } public @NotNull Component errorAlreadyYourAlt(final @NotNull Member alt) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("error.already-your-alt")), Placeholder.unparsed("alt", Optional - .ofNullable(alt.player().getName()).orElse(alt.player().getUniqueId().toString()))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.already-your-alt")), + Placeholder.unparsed("alt", CachedProfile.getName(alt.player())) + ); } public @NotNull Component errorMaxAltsReached(final int max) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("error.max-alts-reached")), Placeholder.unparsed("max", String.valueOf(max))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.max-alts-reached")), + Placeholder.unparsed("max", String.valueOf(max)) + ); } - public @NotNull Component errorMemberNotAlt(final @NotNull Member player) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("error.member-not-alt")), Placeholder.unparsed("player", Optional - .ofNullable(player.player().getName()).orElse(player.player().getUniqueId().toString()))); + public @NotNull Component errorMemberNotAlt(final @NotNull Member member) { + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.member-not-alt")), + Placeholder.unparsed("player", CachedProfile.getName(member.player())) + ); } - public @NotNull Component errorRemoveJoinedAlt(final @NotNull Member player) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("error.remove-joined-alt")), Placeholder.unparsed("player", Optional - .ofNullable(player.player().getName()).orElse(player.player().getUniqueId().toString()))); + public @NotNull Component errorRemoveJoinedAlt(final @NotNull Member member) { + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.remove-joined-alt")), + Placeholder.unparsed("player", CachedProfile.getName(member.player())) + ); } public @NotNull Component errorNeverJoined(final @NotNull OfflinePlayer player) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("error.never-joined")), Placeholder.unparsed("player", Optional - .ofNullable(player.getName()).orElse(player.getUniqueId().toString()))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.never-joined")), + Placeholder.unparsed("player", CachedProfile.getName(player)) + ); } public @NotNull Component errorCommandOnStaff(final @NotNull String label) { - return MiniMessage.miniMessage() - .deserialize(Objects.requireNonNull(config.getString("error.command-on-staff")), Placeholder.unparsed("command", label)); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.command-on-staff")), + Placeholder.unparsed("command", label) + ); } public @NotNull Component errorNotCitizen() { - return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("error.not-citizen-you"))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.not-citizen-you")) + ); } public @NotNull Component errorNotCitizen(final @NotNull Member member) { - return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("error.not-citizen")), Placeholder - .unparsed("player", Optional - .ofNullable(member.player().getName()).orElse(member.player().getUniqueId().toString()))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.not-citizen")), + Placeholder.unparsed("player", CachedProfile.getName(member.player())) + ); } public @NotNull Component errorAlreadyCitizen() { - return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("error.already-citizen"))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.already-citizen")) + ); } public @NotNull Component errorAlreadyCitizen(final @NotNull Nation nation) { - return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("error.already-citizen-nation")), Placeholder.unparsed("nation", nation.name)); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.already-citizen-nation")), + Placeholder.unparsed("nation", nation.name) + ); } public @NotNull Component errorAlreadyCitizen(final @NotNull Member member) { - return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("error.already-citizen-player")), Placeholder.unparsed("player", Optional.ofNullable(member.player().getName()).orElse(member.player().getUniqueId().toString()))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.already-citizen-player")), + Placeholder.unparsed("player", CachedProfile.getName(member.player())) + ); } public @NotNull Component errorOtherCitizen(final @NotNull Member member) { - return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("error.other-citizen")), Placeholder.unparsed("player", Optional.ofNullable(member.player().getName()).orElse(member.player().getUniqueId().toString()))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.other-citizen")), + Placeholder.unparsed("player", CachedProfile.getName(member.player())) + ); } public @NotNull Component errorKickLeadership() { - return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("error.kick-leadership"))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.kick-leadership")) + ); } public @NotNull Component errorLeaderLeave() { - return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("error.leader-leave"))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.leader-leave")) + ); } public @NotNull Component errorNationNotFound(final @NotNull String nation) { - return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("error.nation-not-found")), Placeholder.unparsed("nation", nation)); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.nation-not-found")), + Placeholder.unparsed("nation", nation) + ); } public @NotNull Component errorNotInvited(final @NotNull Nation nation) { - return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("error.not-invited")), Placeholder.unparsed("nation", nation.name)); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.not-invited")), + Placeholder.unparsed("nation", nation.name) + ); } public @NotNull Component errorAlreadyRequestedJoin(final @NotNull Nation nation) { - return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("error.already-requested-join")), Placeholder.unparsed("nation", nation.name)); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.already-requested-join")), + Placeholder.unparsed("nation", nation.name) + ); } public @NotNull Component errorAlreadyInvited(final @NotNull Member member) { - return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("error.already-invited")), Placeholder.unparsed("player", Optional.ofNullable(member.player().getName()).orElse(member.player().getUniqueId().toString()))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.already-invited")), + Placeholder.unparsed("player", CachedProfile.getName(member.player())) + ); } public @NotNull Component errorNoRequest(final @NotNull Nation nation) { - return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("error.no-request-nation")), Placeholder.unparsed("nation", nation.name)); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.no-request-nation")), + Placeholder.unparsed("nation", nation.name) + ); } public @NotNull Component errorNoRequest(final @NotNull Member member) { - return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("error.no-request-player")), Placeholder.unparsed("player", Optional.ofNullable(member.player().getName()).orElse(member.player().getUniqueId().toString()))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.no-request-player")), + Placeholder.unparsed("player", CachedProfile.getName(member.player())) + ); } public @NotNull Component errorViceConflict() { - return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("error.vice-conflict"))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.vice-conflict")) + ); } public @NotNull Component errorAlreadyVice(final @NotNull Member member) { - return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("error.already-vice")), Placeholder.unparsed("player", Optional.ofNullable(member.player().getName()).orElse(member.player().getUniqueId().toString()))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.already-vice")), + Placeholder.unparsed("player", CachedProfile.getName(member.player())) + ); } public @NotNull Component errorDemoteLeader() { - return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("error.demote-leader"))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.demote-leader")) + ); } public @NotNull Component errorDemoteCitizen() { - return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("error.demote-citizen"))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.demote-citizen")) + ); } public @NotNull Component errorDurationZeroOrLess() { - return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("error.duration-zero-or-less"))); + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.duration-zero-or-less")) + ); } public @NotNull Component errorInvalidDuration(final @NotNull String duration) { @@ -703,16 +871,14 @@ public Messages() { public @NotNull Component errorAlreadyMember(final @NotNull Member member) { return MiniMessage.miniMessage().deserialize( Objects.requireNonNull(config.getString("error.already-member")), - Placeholder.unparsed("player", Optional.ofNullable(member.player().getName()) - .orElse(member.player().getUniqueId().toString())) + Placeholder.unparsed("player", CachedProfile.getName(member.player())) ); } public @NotNull Component errorAlreadyStaff(final @NotNull Member member) { return MiniMessage.miniMessage().deserialize( Objects.requireNonNull(config.getString("error.already-staff")), - Placeholder.unparsed("player", Optional.ofNullable(member.player().getName()) - .orElse(member.player().getUniqueId().toString())), + Placeholder.unparsed("player", CachedProfile.getName(member.player())), Formatter.choice("staff", member.staff ? 1 : 0) ); } @@ -720,30 +886,35 @@ public Messages() { public @NotNull Component errorRemoveMemberLeader(final @NotNull Member member, final @NotNull Nation nation) { return MiniMessage.miniMessage().deserialize( Objects.requireNonNull(config.getString("error.remove-member-leader")), - Placeholder.unparsed("player", Optional.ofNullable(member.player().getName()) - .orElse(member.player().getUniqueId().toString())), + Placeholder.unparsed("player", CachedProfile.getName(member.player())), Placeholder.unparsed("nation", nation.name) ); } public record SubCommandArgument(@NotNull String name, boolean required) { public @NotNull Component component() { - return required ? SMPCore.messages().subCommandArgumentRequired(name) : SMPCore.messages() - .subCommandArgumentOptional(name); + return required + ? SMPCore.messages().subCommandArgumentRequired(name) + : SMPCore.messages().subCommandArgumentOptional(name); } public static @NotNull Component join(final @NotNull SubCommandArgument @NotNull [] args) { - final @NotNull TextComponent.Builder builder = Component.text().append(Component.text(" ")); + final TextComponent.Builder builder = Component.text().append(Component.text(" ")); for (int i = 0; i < args.length; ++i) { builder.append(args[i].component()); - if (i < args.length - 1) builder.append(Component.text(" ")); + + if (i < args.length - 1) + builder.append(Component.text(" ")); } + return builder.build(); } public static @NotNull SubCommandArgument @NotNull [] of(final @Nullable SubCommandArgument @NotNull ... args) { - return Arrays.stream(args).filter(Objects::nonNull).toArray(SubCommandArgument[]::new); + return Arrays.stream(args) + .filter(Objects::nonNull) + .toArray(SubCommandArgument[]::new); } } } diff --git a/src/main/java/pro/cloudnode/smp/smpcore/REST.java b/src/main/java/pro/cloudnode/smp/smpcore/REST.java index 890c44c..61b023d 100644 --- a/src/main/java/pro/cloudnode/smp/smpcore/REST.java +++ b/src/main/java/pro/cloudnode/smp/smpcore/REST.java @@ -29,7 +29,7 @@ private void e404 (final @NotNull io.javalin.http.Context ctx) { final @NotNull JsonObject obj = new JsonObject(); final @NotNull OfflinePlayer player = member.player(); obj.addProperty("uuid", member.uuid.toString()); - obj.addProperty("name", player.getName()); + obj.addProperty("name", CachedProfile.getName(player)); obj.addProperty("nation", member.nationID); obj.addProperty("staff", member.staff); obj.addProperty("online", !member.staff && player.isOnline()); @@ -159,7 +159,7 @@ public REST(final int port) { final @NotNull JsonObject altObj = new JsonObject(); final @NotNull OfflinePlayer player = alt.player(); altObj.addProperty("uuid", alt.uuid.toString()); - altObj.addProperty("name", player.getName()); + altObj.addProperty("name", CachedProfile.getName(player)); altObj.addProperty("nation", alt.nationID); altObj.addProperty("added", alt.added.getTime()); altObj.addProperty("lastSeen", alt.staff ? 0 : player.getLastSeen()); diff --git a/src/main/java/pro/cloudnode/smp/smpcore/SMPCore.java b/src/main/java/pro/cloudnode/smp/smpcore/SMPCore.java index e8ed2a2..a6fc47c 100644 --- a/src/main/java/pro/cloudnode/smp/smpcore/SMPCore.java +++ b/src/main/java/pro/cloudnode/smp/smpcore/SMPCore.java @@ -2,7 +2,9 @@ import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; +import io.papermc.paper.util.Tick; import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; import org.bukkit.World; import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.NotNull; @@ -16,8 +18,8 @@ import pro.cloudnode.smp.smpcore.command.SeenCommand; import pro.cloudnode.smp.smpcore.command.TimeCommand; import pro.cloudnode.smp.smpcore.command.UnbanCommand; -import pro.cloudnode.smp.smpcore.listener.PlayerJoinListener; import pro.cloudnode.smp.smpcore.listener.PlayerDeathListener; +import pro.cloudnode.smp.smpcore.listener.PlayerJoinListener; import pro.cloudnode.smp.smpcore.listener.PlayerPostRespawnListener; import pro.cloudnode.smp.smpcore.listener.PlayerPreLoginListener; import pro.cloudnode.smp.smpcore.listener.PlayerServerFullCheckListener; @@ -92,6 +94,13 @@ public void onEnable() { commands.put("alts", new AltsCommand(commands.get("smpcore"))); for (final Map.Entry entry : commands.entrySet()) Objects.requireNonNull(getServer().getPluginCommand(entry.getKey())).setExecutor(entry.getValue()); + + Bukkit.getScheduler().runTaskTimerAsynchronously( + this, + CachedProfile::cleanUp, + 0L, + Tick.tick().fromDuration(CachedProfile.STALE_WHILE_REVALIDATE) + ); } @Override @@ -235,4 +244,5 @@ public static boolean ifDisallowedCharacters(final @NotNull String source, final public static @NotNull Date gameTime(final long ticks) { return new Date(ticks * 3600 + 21600000); } + } diff --git a/src/main/java/pro/cloudnode/smp/smpcore/command/BanCommand.java b/src/main/java/pro/cloudnode/smp/smpcore/command/BanCommand.java index d71ede5..2e5fea3 100644 --- a/src/main/java/pro/cloudnode/smp/smpcore/command/BanCommand.java +++ b/src/main/java/pro/cloudnode/smp/smpcore/command/BanCommand.java @@ -5,6 +5,7 @@ import org.bukkit.command.CommandSender; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import pro.cloudnode.smp.smpcore.CachedProfile; import pro.cloudnode.smp.smpcore.Member; import pro.cloudnode.smp.smpcore.Permission; import pro.cloudnode.smp.smpcore.SMPCore; @@ -16,7 +17,6 @@ import java.util.Arrays; import java.util.Date; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -102,7 +102,7 @@ public boolean run(@NotNull CommandSender sender, @NotNull String label, @NotNul ? String.join(" ", Arrays.copyOfRange(args, duration == null ? 1 : 2, args.length)) : null; - final @NotNull OfflinePlayer target = SMPCore.getInstance().getServer().getOfflinePlayer(args[0]); + final @NotNull OfflinePlayer target = CachedProfile.get(args[0]); final List banned = ban( target, @@ -121,10 +121,13 @@ public boolean run(@NotNull CommandSender sender, @NotNull String label, @NotNul ); } - @SuppressWarnings("NullableProblems") @Override public @NotNull List<@NotNull String> tab(@NotNull CommandSender sender, @NotNull String label, @NotNull String @NotNull [] args) { - if (args.length <= 1) return Arrays.stream(SMPCore.getInstance().getServer().getOfflinePlayers()).filter(p -> !p.isBanned()).map(OfflinePlayer::getName).filter(Objects::nonNull).toList(); + if (args.length <= 1) + return Arrays.stream(SMPCore.getInstance().getServer().getOfflinePlayers()) + .filter(p -> !p.isBanned()) + .map(CachedProfile::getName) + .toList(); return List.of(); } } diff --git a/src/main/java/pro/cloudnode/smp/smpcore/command/MainCommand.java b/src/main/java/pro/cloudnode/smp/smpcore/command/MainCommand.java index aceee3f..3d11912 100644 --- a/src/main/java/pro/cloudnode/smp/smpcore/command/MainCommand.java +++ b/src/main/java/pro/cloudnode/smp/smpcore/command/MainCommand.java @@ -11,6 +11,7 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import pro.cloudnode.smp.smpcore.CachedProfile; import pro.cloudnode.smp.smpcore.Member; import pro.cloudnode.smp.smpcore.Messages; import pro.cloudnode.smp.smpcore.Nation; @@ -71,7 +72,8 @@ else switch (args[1]) { if (sender instanceof final @NotNull Player player) { final @NotNull Optional<@NotNull Member> member = Member.get(player); member.ifPresent(value -> suggestions.addAll(value.getAlts().stream() - .map(m -> m.player().getName()).filter(Objects::nonNull).toList())); + .map(m -> CachedProfile.getName(m.player())) + .toList())); } } } @@ -158,10 +160,11 @@ else switch (originalArgs[0]) { final @NotNull OfflinePlayer target; if (!(sender instanceof final @NotNull Player player)) { if (originalArgs.length == 1) return sendMessage(sender, SMPCore.messages().usage(label, "list ")); - target = sender.getServer().getOfflinePlayer(originalArgs[1]); + target = CachedProfile.get(originalArgs[1]); } else { - if (originalArgs.length > 1 && player.hasPermission(Permission.ALT_OTHER)) target = player.getServer().getOfflinePlayer(originalArgs[1]); + if (originalArgs.length > 1 && player.hasPermission(Permission.ALT_OTHER)) + target = CachedProfile.get(originalArgs[1]); else target = player; } final @NotNull Optional<@NotNull Member> targetMember = Member.get(target); @@ -200,10 +203,11 @@ else switch (originalArgs[0]) { final @NotNull OfflinePlayer target; if (!(sender instanceof final @NotNull Player player)) { if (args.length == 2) return sendMessage(sender, SMPCore.messages().usage(label, "add ")); - target = sender.getServer().getOfflinePlayer(args[2]); + target = CachedProfile.get(args[2]); } else { - if (args.length > 2 && player.hasPermission(Permission.ALT_ADD_OTHER)) target = player.getServer().getOfflinePlayer(args[2]); + if (args.length > 2 && player.hasPermission(Permission.ALT_ADD_OTHER)) + target = CachedProfile.get(args[2]); else target = player; } final @NotNull Optional<@NotNull Member> tempTargetMember = Member.get(target); @@ -220,7 +224,7 @@ else switch (originalArgs[0]) { if (!sender.hasPermission(Permission.ALT_MAX_BYPASS) && targetMember.getAlts().size() >= SMPCore.config().altsMax()) return sendMessage(sender, SMPCore.messages().errorMaxAltsReached(SMPCore.config().altsMax())); - final @NotNull OfflinePlayer altPlayer = sender.getServer().getOfflinePlayer(args[1]); + final @NotNull OfflinePlayer altPlayer = CachedProfile.get(args[1]); final @NotNull Optional<@NotNull Member> altMember = Member.get(altPlayer); if (altMember.isPresent()) { if (altMember.get().isAlt() && Objects.requireNonNull(altMember.get().altOwnerUUID).equals(target.getUniqueId())) @@ -245,7 +249,7 @@ else switch (originalArgs[0]) { if (originalArgs.length == 1) return sendMessage(sender, SMPCore.messages().usage(label, "remove ")); - final @NotNull OfflinePlayer altPlayer = sender.getServer().getOfflinePlayer(originalArgs[1]); + final @NotNull OfflinePlayer altPlayer = CachedProfile.get(originalArgs[1]); final @NotNull Optional<@NotNull Member> altMember = Member.get(altPlayer); if (altMember.isEmpty()) return sendMessage(sender, SMPCore.messages().errorNotMember(altPlayer)); @@ -306,7 +310,7 @@ public static boolean member( if (args.length != 2) return sendMessage(sender, SMPCore.messages().usage(label, "member add ")); - final OfflinePlayer target = sender.getServer().getOfflinePlayer(args[1]); + final OfflinePlayer target = CachedProfile.get(args[1]); final Optional existing = Member.get(target); if (existing.isPresent()) @@ -343,7 +347,7 @@ public static boolean member( if (args.length != 2) return sendMessage(sender, SMPCore.messages().usage(label, "member remove ")); - final OfflinePlayer target = sender.getServer().getOfflinePlayer(args[1]); + final OfflinePlayer target = CachedProfile.get(args[1]); final Optional member = Member.get(target); if (member.isEmpty()) @@ -362,7 +366,7 @@ public static boolean member( if (args.length != 3) return sendMessage(sender, SMPCore.messages().usage(label, "member staff " + (args.length > 1 ? args[1] : "") + " ")); - final OfflinePlayer target = sender.getServer().getOfflinePlayer(args[1]); + final OfflinePlayer target = CachedProfile.get(args[1]); final boolean requestedStatus; switch (args[2].toLowerCase()) { diff --git a/src/main/java/pro/cloudnode/smp/smpcore/command/NationCommand.java b/src/main/java/pro/cloudnode/smp/smpcore/command/NationCommand.java index c19c817..20e033e 100644 --- a/src/main/java/pro/cloudnode/smp/smpcore/command/NationCommand.java +++ b/src/main/java/pro/cloudnode/smp/smpcore/command/NationCommand.java @@ -7,6 +7,7 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import pro.cloudnode.smp.smpcore.CachedProfile; import pro.cloudnode.smp.smpcore.CitizenRequest; import pro.cloudnode.smp.smpcore.Member; import pro.cloudnode.smp.smpcore.Messages; @@ -20,7 +21,6 @@ import java.util.Collections; import java.util.Date; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.stream.Stream; @@ -159,9 +159,10 @@ else switch (args[0]) { if (!sender.hasPermission(Permission.NATION_CITIZENS_KICK)) break; list.addAll(nation.get().citizens().stream() - .filter(c -> !c.uuid.equals(nation.get().leaderUUID)) - .map(c -> c.player().getName()) - .filter(Objects::nonNull).toList()); + .filter(c -> !c.uuid.equals(nation.get().leaderUUID)) + .map(c -> CachedProfile.getName(c.player())) + .toList() + ); } case "promote" -> { if (other && !sender.hasPermission(Permission.NATION_PROMOTE_OTHER)) @@ -169,9 +170,10 @@ else switch (args[0]) { if (!sender.hasPermission(Permission.NATION_PROMOTE)) break; list.addAll(nation.get().citizens().stream() - .filter(c -> !(c.uuid.equals(nation.get().leaderUUID) || c.uuid.equals(nation.get().viceLeaderUUID))) - .map(c -> c.player().getName()) - .filter(Objects::nonNull).toList()); + .filter(c -> !(c.uuid.equals(nation.get().leaderUUID) || + c.uuid.equals(nation.get().viceLeaderUUID))) + .map(c -> CachedProfile.getName(c.player())) + .toList()); } case "demote" -> { if (other && !sender.hasPermission(Permission.NATION_DEMOTE_OTHER)) @@ -181,9 +183,8 @@ else switch (args[0]) { final @NotNull Member vice = nation.get().vice(); if (vice.uuid.equals(nation.get().leaderUUID)) break; - final var name = vice.player().getName(); - if (name != null) - list.add(name); + + list.add(CachedProfile.getName(vice.player())); } case "invite", "request", "req" -> { if (other && !sender.hasPermission(Permission.NATION_INVITE_OTHER)) @@ -192,8 +193,8 @@ else switch (args[0]) { break; list.addAll(Member.get().stream() .filter(m -> !nation.get().id.equals(m.nationID)) - .map(c -> c.player().getName()) - .filter(Objects::nonNull).toList()); + .map(c -> CachedProfile.getName(c.player())) + .toList()); } case "cancel", "reject", "decline", "withdraw", "refuse", "deny" -> { if (other && !sender.hasPermission(Permission.NATION_INVITE_OTHER)) @@ -203,7 +204,9 @@ else switch (args[0]) { list.addAll(Stream.concat( CitizenRequest.get(nation.get(), true).stream(), CitizenRequest.get(nation.get(), false).stream() - ).map(req -> req.member().player().getName()).filter(Objects::nonNull).sorted().toList()); + ).map(req -> CachedProfile.getName(req.member().player())) + .sorted() + .toList()); } case "add" -> { if (other && !sender.hasPermission(Permission.NATION_CITIZEN_ADD_OTHER)) @@ -211,9 +214,9 @@ else switch (args[0]) { if (!sender.hasPermission(Permission.NATION_CITIZEN_ADD)) break; list.addAll(Member.get().stream() - .filter(m -> !nation.get().id.equals(m.nationID)) - .map(c -> c.player().getName()) - .filter(Objects::nonNull).toList()); + .filter(m -> !nation.get().id.equals(m.nationID)) + .map(c -> CachedProfile.getName(c.player())) + .toList()); } } } @@ -433,7 +436,7 @@ public static boolean kickCitizen( if (args.length == 0) return sendMessage(sender, SMPCore.messages().usage(label, "")); - final @NotNull OfflinePlayer target = sender.getServer().getOfflinePlayer(args[0]); + final @NotNull OfflinePlayer target = CachedProfile.get(args[0]); final @NotNull Optional<@NotNull Member> targetMemberOptional = Member.get(target); if (targetMemberOptional.isEmpty()) return sendMessage(sender, SMPCore.messages().errorNotMember(target)); @@ -464,7 +467,7 @@ public static boolean promoteCitizen( if (args.length == 0) return sendMessage(sender, SMPCore.messages().usage(label, "")); - final @NotNull OfflinePlayer target = sender.getServer().getOfflinePlayer(args[0]); + final @NotNull OfflinePlayer target = CachedProfile.get(args[0]); final @NotNull Optional<@NotNull Member> targetMember = Member.get(target); if (targetMember.isEmpty()) return sendMessage(sender, SMPCore.messages().errorNotMember(target)); @@ -509,7 +512,7 @@ public static boolean demoteViceLeader( if (args.length == 0) return sendMessage(sender, SMPCore.messages().usage(label, "")); - final @NotNull OfflinePlayer target = sender.getServer().getOfflinePlayer(args[0]); + final @NotNull OfflinePlayer target = CachedProfile.get(args[0]); final @NotNull Optional<@NotNull Member> targetMemberOptional = Member.get(target); if (targetMemberOptional.isEmpty()) return sendMessage(sender, SMPCore.messages().errorNotMember(target)); @@ -552,7 +555,7 @@ public static boolean inviteCitizen( if (args.length < 1) return sendMessage(sender, SMPCore.messages().usage(label, "")); - final var targetPlayer = sender.getServer().getOfflinePlayer(args[0]); + final var targetPlayer = CachedProfile.get(args[0]); final @NotNull var target = Member.get(targetPlayer); if (target.isEmpty()) @@ -604,7 +607,7 @@ public static boolean addCitizen( if (args.length < 1) return sendMessage(sender, SMPCore.messages().usage(label, "")); - final @NotNull var targetPlayer = sender.getServer().getOfflinePlayer(args[0]); + final @NotNull var targetPlayer = CachedProfile.get(args[0]); final @NotNull var target = Member.get(targetPlayer); if (target.isEmpty()) @@ -737,7 +740,7 @@ public static boolean nationCancel( if (args.length < 1) return sendMessage(sender, SMPCore.messages().usage(label, "")); - final var targetPlayer = sender.getServer().getOfflinePlayer(args[0]); + final var targetPlayer = CachedProfile.get(args[0]); final @NotNull var target = Member.get(targetPlayer); if (target.isEmpty()) diff --git a/src/main/java/pro/cloudnode/smp/smpcore/command/SeenCommand.java b/src/main/java/pro/cloudnode/smp/smpcore/command/SeenCommand.java index b7b0957..a20a045 100644 --- a/src/main/java/pro/cloudnode/smp/smpcore/command/SeenCommand.java +++ b/src/main/java/pro/cloudnode/smp/smpcore/command/SeenCommand.java @@ -3,12 +3,12 @@ import org.bukkit.OfflinePlayer; import org.bukkit.command.CommandSender; import org.jetbrains.annotations.NotNull; +import pro.cloudnode.smp.smpcore.CachedProfile; import pro.cloudnode.smp.smpcore.Member; import pro.cloudnode.smp.smpcore.Permission; import pro.cloudnode.smp.smpcore.SMPCore; import java.util.List; -import java.util.Objects; import java.util.Optional; public final class SeenCommand extends Command { @@ -19,7 +19,7 @@ public boolean run(@NotNull CommandSender sender, @NotNull String label, @NotNul if (args.length != 1) return sendMessage(sender, SMPCore.messages().usage(label, "")); - final @NotNull OfflinePlayer player = sender.getServer().getOfflinePlayer(args[0]); + final @NotNull OfflinePlayer player = CachedProfile.get(args[0]); if (!player.hasPlayedBefore()) return sendMessage(sender, SMPCore.messages().errorNeverJoined(player)); @@ -31,10 +31,12 @@ public boolean run(@NotNull CommandSender sender, @NotNull String label, @NotNul .orElseGet(() -> sendMessage(sender, SMPCore.messages().seen(player))); } - @SuppressWarnings("NullableProblems") @Override public @NotNull List<@NotNull String> tab(@NotNull CommandSender sender, @NotNull String label, @NotNull String @NotNull [] args) { if (sender.hasPermission(Permission.SEEN_STAFF)) return Member.getNames().stream().toList(); - else return Member.get().stream().filter(m -> !m.staff).map(m -> m.player().getName()).filter(Objects::nonNull).toList(); + else return Member.get().stream() + .filter(m -> !m.staff) + .map(m -> CachedProfile.getName(m.player())) + .toList(); } } diff --git a/src/main/java/pro/cloudnode/smp/smpcore/command/UnbanCommand.java b/src/main/java/pro/cloudnode/smp/smpcore/command/UnbanCommand.java index 9bbc375..bfa2953 100644 --- a/src/main/java/pro/cloudnode/smp/smpcore/command/UnbanCommand.java +++ b/src/main/java/pro/cloudnode/smp/smpcore/command/UnbanCommand.java @@ -6,6 +6,7 @@ import org.bukkit.OfflinePlayer; import org.bukkit.command.CommandSender; import org.jetbrains.annotations.NotNull; +import pro.cloudnode.smp.smpcore.CachedProfile; import pro.cloudnode.smp.smpcore.Member; import pro.cloudnode.smp.smpcore.Permission; import pro.cloudnode.smp.smpcore.SMPCore; @@ -23,7 +24,7 @@ public final class UnbanCommand extends Command { public boolean run(@NotNull CommandSender sender, @NotNull String label, @NotNull String @NotNull [] args) { if (!sender.hasPermission(Permission.BAN)) return sendMessage(sender, SMPCore.messages().errorNoPermission()); if (args.length < 1) return sendMessage(sender, SMPCore.messages().usage(label, "")); - final @NotNull OfflinePlayer target = SMPCore.getInstance().getServer().getOfflinePlayer(args[0]); + final @NotNull OfflinePlayer target = CachedProfile.get(args[0]); final @NotNull Optional<@NotNull Member> targetMember = Member.get(target); if (targetMember.isEmpty()) { if (!target.isBanned()) return sendMessage(sender, SMPCore.messages().errorPlayerNotBanned(target)); diff --git a/src/main/resources/init.sql b/src/main/resources/init.sql index 1ef0f56..4333a34 100644 --- a/src/main/resources/init.sql +++ b/src/main/resources/init.sql @@ -1,37 +1,52 @@ -CREATE TABLE IF NOT EXISTS `members` ( - `uuid` CHAR(36) PRIMARY KEY NOT NULL COLLATE NOCASE, - `nation` CHAR(2) DEFAULT NULL COLLATE NOCASE, - `staff` TINYINT(1) NOT NULL DEFAULT 0, - `alt_owner` CHAR(36) DEFAULT NULL COLLATE NOCASE, - `added` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +CREATE TABLE IF NOT EXISTS `members` +( + `uuid` CHAR(36) PRIMARY KEY NOT NULL COLLATE NOCASE, + `nation` CHAR(2) DEFAULT NULL COLLATE NOCASE, + `staff` TINYINT(1) NOT NULL DEFAULT 0, + `alt_owner` CHAR(36) DEFAULT NULL COLLATE NOCASE, + `added` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); -CREATE TABLE IF NOT EXISTS `tokens` ( - `token` CHAR(36) PRIMARY KEY NOT NULL COLLATE NOCASE, - `member` CHAR(36) NOT NULL COLLATE NOCASE, - `created` CHAR(36) NOT NULL COLLATE NOCASE, - `last_used` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +CREATE TABLE IF NOT EXISTS `tokens` +( + `token` CHAR(36) PRIMARY KEY NOT NULL COLLATE NOCASE, + `member` CHAR(36) NOT NULL COLLATE NOCASE, + `created` CHAR(36) NOT NULL COLLATE NOCASE, + `last_used` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); -CREATE TABLE IF NOT EXISTS `nations` ( - `id` CHAR(2) PRIMARY KEY NOT NULL COLLATE NOCASE, - `name` VARCHAR(128) NOT NULL COLLATE NOCASE, - `short_name` VARCHAR(16) NOT NULL COLLATE NOCASE, - `color` CHAR(6) NOT NULL COLLATE NOCASE, - `leader` CHAR(36) NOT NULL COLLATE NOCASE, - `vice` CHAR(36) NOT NULL COLLATE NOCASE, - `founded` DATETIME NOT NULL, - `founded_ticks` INTEGER NOT NULL, - `bank` CHAR(16) NOT NULL COLLATE BINARY +CREATE TABLE IF NOT EXISTS `nations` +( + `id` CHAR(2) PRIMARY KEY NOT NULL COLLATE NOCASE, + `name` VARCHAR(128) NOT NULL COLLATE NOCASE, + `short_name` VARCHAR(16) NOT NULL COLLATE NOCASE, + `color` CHAR(6) NOT NULL COLLATE NOCASE, + `leader` CHAR(36) NOT NULL COLLATE NOCASE, + `vice` CHAR(36) NOT NULL COLLATE NOCASE, + `founded` DATETIME NOT NULL, + `founded_ticks` INTEGER NOT NULL, + `bank` CHAR(16) NOT NULL COLLATE BINARY ); -CREATE TABLE IF NOT EXISTS `citizen_requests` ( - `member` CHAR(36) NOT NULL COLLATE NOCASE, - `nation` CHAR(2) NOT NULL COLLATE NOCASE, - `mode` TINYINT(1) NOT NULL, -- 0 = nation invites, 1 = member requests - `created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - `expires` DATETIME NOT NULL, +CREATE TABLE IF NOT EXISTS `citizen_requests` +( + `member` CHAR(36) NOT NULL COLLATE NOCASE, + `nation` CHAR(2) NOT NULL COLLATE NOCASE, + `mode` TINYINT(1) NOT NULL, -- 0 = nation invites, 1 = member requests + `created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `expires` DATETIME NOT NULL, PRIMARY KEY (`member`, `nation`) ); -DELETE from `citizen_requests` WHERE `expires` >= CURRENT_TIMESTAMP; +DELETE +from `citizen_requests` +WHERE `expires` >= CURRENT_TIMESTAMP; + +CREATE TABLE IF NOT EXISTS `cached_profiles` +( + `uuid` CHAR(36) NOT NULL COLLATE NOCASE, + `name` VARCHAR(16) NOT NULL COLLATE NOCASE, + `fetched` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`uuid`), + UNIQUE (`name`) +);