diff --git a/src/main/java/pro/cloudnode/smp/smpcore/Configuration.java b/src/main/java/pro/cloudnode/smp/smpcore/Configuration.java index 71f4e9f..87f3d2e 100644 --- a/src/main/java/pro/cloudnode/smp/smpcore/Configuration.java +++ b/src/main/java/pro/cloudnode/smp/smpcore/Configuration.java @@ -42,7 +42,7 @@ public int joinRequestExpireMinutes() { return config.getInt("join.request-expire-minutes"); } - public @NotNull Component relativeTime(final int t, final @NotNull ChronoUnit unit) { + public @NotNull Component relativeTime(final Number t, final @NotNull ChronoUnit unit) { final @NotNull String formatString = Objects.requireNonNull(config.getString("relative-time." + switch (unit) { case SECONDS -> "seconds"; case MINUTES -> "minutes"; @@ -54,8 +54,10 @@ public int joinRequestExpireMinutes() { throw new IllegalStateException("No relative time format for ChronoUnit " + unit); } })); - return MiniMessage.miniMessage() - .deserialize(formatString, Formatter.number("t", t), Formatter.choice("format", Math.abs(t))); + return MiniMessage.miniMessage().deserialize(formatString, + Formatter.number("t", t), + Formatter.choice("format", Math.abs(t.doubleValue())) + ); } public @NotNull Component relativeTimeFuture(final @NotNull Component relativeTime) { @@ -67,4 +69,18 @@ public int joinRequestExpireMinutes() { return MiniMessage.miniMessage() .deserialize(Objects.requireNonNull(config.getString("relative-time.past")), Placeholder.component("t", relativeTime)); } + + public @NotNull Component relativeTimeDuration(final @NotNull Component duration) { + return MiniMessage.miniMessage() + .deserialize( + Objects.requireNonNull(config.getString("relative-time.duration")), + Placeholder.component("t", duration) + ); + } + + public @NotNull Component relativeTimeDurationIndefinite() { + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("relative-time.duration-indefinite")) + ); + } } diff --git a/src/main/java/pro/cloudnode/smp/smpcore/Messages.java b/src/main/java/pro/cloudnode/smp/smpcore/Messages.java index 8931c13..57bc1a9 100644 --- a/src/main/java/pro/cloudnode/smp/smpcore/Messages.java +++ b/src/main/java/pro/cloudnode/smp/smpcore/Messages.java @@ -1,6 +1,7 @@ package pro.cloudnode.smp.smpcore; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.JoinConfiguration; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.tag.resolver.Formatter; @@ -10,7 +11,9 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.time.Duration; import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -22,7 +25,7 @@ import java.util.TimeZone; import java.util.stream.Collectors; -public class Messages extends BaseConfig { +public final class Messages extends BaseConfig { public Messages() { super("messages.yml"); @@ -37,26 +40,70 @@ public Messages() { .deserialize(Objects.requireNonNull(config.getString("usage")), Placeholder.unparsed("label", label), Placeholder.unparsed("args", args)); } - public @NotNull Component bannedPlayer(final @NotNull OfflinePlayer player) { + private @NotNull Component formatDuration(@Nullable Duration duration) { + if (duration == null) return SMPCore.config().relativeTimeDurationIndefinite(); + + final long seconds = Math.abs(duration.getSeconds()); + final long days = seconds / 86_400; + final long hours = (seconds % 86_400) / 3_600; + final long minutes = (seconds % 3_600) / 60; + final long secs = seconds % 60; + + final ArrayList components = new ArrayList<>(); + + if (days > 0) components.add(SMPCore.config().relativeTime(days, ChronoUnit.DAYS)); + if (hours > 0) components.add(SMPCore.config().relativeTime(hours, ChronoUnit.HOURS)); + if (minutes > 0) components.add(SMPCore.config().relativeTime(minutes, ChronoUnit.MINUTES)); + if (secs > 0) components.add(SMPCore.config().relativeTime(secs, ChronoUnit.SECONDS)); + + final Component joined = Component.join(JoinConfiguration.spaces(), components); + + return SMPCore.config().relativeTimeDuration(joined); + } + + 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()))); + .deserialize( + Objects.requireNonNull(config.getString("banned-player")), + Placeholder.unparsed("player", + Optional.ofNullable(player.getName()) + .orElse(player.getUniqueId().toString()) + ), + Placeholder.component("duration", formatDuration(duration)) + ); } - public @NotNull Component bannedMember(final @NotNull Member member) { + 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()))); + .deserialize( + Objects.requireNonNull(config.getString("banned-member")), + Placeholder.unparsed("player", + Optional.ofNullable(member.player().getName()) + .orElse(member.player().getUniqueId().toString()) + ), + Placeholder.component("duration", formatDuration(duration)) + ); } - public @NotNull Component bannedMemberChain(final @NotNull Member member, final @NotNull List<@NotNull Member> alts) { + public @NotNull Component bannedMemberChain( + final @NotNull Member member, + 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())) .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("n-alt", String.valueOf(alts.size())), Placeholder.unparsed("alts", altsString)); + .deserialize( + Objects.requireNonNull(config.getString("banned-member-chain")), + Placeholder.unparsed("player", + Optional.ofNullable(member.player().getName()) + .orElse(member.player().getUniqueId().toString()) + ), + Placeholder.unparsed("n-alt", String.valueOf(alts.size())), + Placeholder.unparsed("alts", altsString), + Placeholder.component("duration", formatDuration(duration)) + ); } public @NotNull Component unbannedPlayer(final @NotNull OfflinePlayer player) { @@ -568,6 +615,17 @@ public Messages() { 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"))); + } + + public @NotNull Component invalidDuration(final @NotNull String duration) { + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("error.invalid-duration")), + Placeholder.unparsed("duration", duration) + ); + } + public record SubCommandArgument(@NotNull String name, boolean required) { public @NotNull Component component() { return required ? SMPCore.messages().subCommandArgumentRequired(name) : SMPCore.messages() diff --git a/src/main/java/pro/cloudnode/smp/smpcore/SMPCore.java b/src/main/java/pro/cloudnode/smp/smpcore/SMPCore.java index 3a4b2b4..57ad51c 100644 --- a/src/main/java/pro/cloudnode/smp/smpcore/SMPCore.java +++ b/src/main/java/pro/cloudnode/smp/smpcore/SMPCore.java @@ -188,7 +188,7 @@ public static boolean ifDisallowedCharacters(final @NotNull String source, final final double years = Math.floor(months / 12.0); final @NotNull Component t; - if (years > 0) t = SMPCore.config().relativeTime((int) years, ChronoUnit.YEARS); + if (years > 0) t = SMPCore.config().relativeTime(years, ChronoUnit.YEARS); else if (months > 0) t = SMPCore.config().relativeTime((int) months, ChronoUnit.MONTHS); else if (days > 0) t = SMPCore.config().relativeTime((int) days, ChronoUnit.DAYS); else if (hours > 0) t = SMPCore.config().relativeTime((int) hours, ChronoUnit.HOURS); 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 bcfbbf0..5f5cb83 100644 --- a/src/main/java/pro/cloudnode/smp/smpcore/command/BanCommand.java +++ b/src/main/java/pro/cloudnode/smp/smpcore/command/BanCommand.java @@ -10,6 +10,9 @@ import pro.cloudnode.smp.smpcore.Permission; import pro.cloudnode.smp.smpcore.SMPCore; +import java.time.Duration; +import java.time.Instant; +import java.time.format.DateTimeParseException; import java.util.Arrays; import java.util.Date; import java.util.HashSet; @@ -19,38 +22,54 @@ public final class BanCommand extends Command { /** - * Usage: {@code / [reason]} + * Usage: {@code / [duration] [reason]} */ @Override 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, " [reason]")); - final @NotNull OfflinePlayer target = SMPCore.getInstance().getServer().getOfflinePlayer(args[0]); + if (args.length < 1) + return sendMessage(sender, SMPCore.messages().usage(label, " [duration] [reason]")); + + final @Nullable String durationArg = args.length > 1 ? args[1] : null; + @Nullable Duration duration = null; + if (durationArg != null && durationArg.matches("(?i)^PT\\d.*")) try { + duration = Duration.parse(durationArg); + } + catch (DateTimeParseException ignored) { + return sendMessage(sender, SMPCore.messages().invalidDuration(durationArg)); + } - final @Nullable String reason = args.length > 1 ? String.join(" ", Arrays.copyOfRange(args, 1, args.length)) : null; - final @Nullable Date banExpiry = null; + if (duration != null && (duration.isNegative() || duration.isZero())) + return sendMessage(sender, SMPCore.messages().errorDurationZeroOrLess()); + + final @Nullable Date banExpiry = duration == null ? null : Date.from(Instant.now().plus(duration)); + + final @Nullable String reason = args.length > 1 + ? String.join(" ", Arrays.copyOfRange(args, duration == null ? 1 : 2, args.length)) + : null; final @NotNull NamespacedKey banSource; if (sender instanceof final @NotNull Player player) banSource = new NamespacedKey(SMPCore.getInstance(), "player/" + player.getUniqueId()); else banSource = new NamespacedKey(SMPCore.getInstance(), "console"); + final @NotNull OfflinePlayer target = SMPCore.getInstance().getServer().getOfflinePlayer(args[0]); final @NotNull Optional<@NotNull Member> targetMember = Member.get(target); if (targetMember.isEmpty()) { SMPCore.runMain(() -> target.ban(reason, banExpiry, banSource.asString())); - return sendMessage(sender, SMPCore.messages().bannedPlayer(target)); + return sendMessage(sender, SMPCore.messages().bannedPlayer(target, duration)); } final @NotNull Member main = targetMember.get().altOwner().orElse(targetMember.get()); final @NotNull HashSet<@NotNull Member> alts = main.getAlts(); SMPCore.runMain(() -> main.player().ban(reason, banExpiry, banSource.asString())); - if (alts.isEmpty()) return sendMessage(sender, SMPCore.messages().bannedMember(main)); + if (alts.isEmpty()) return sendMessage(sender, SMPCore.messages().bannedMember(main, duration)); else { SMPCore.runMain(() -> { for (final @NotNull Member alt : alts) alt.player().ban(reason, banExpiry, banSource.asString()); }); - return sendMessage(sender, SMPCore.messages().bannedMemberChain(main, alts.stream().toList())); + return sendMessage(sender, SMPCore.messages().bannedMemberChain(main, alts.stream().toList(), duration)); } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index f7dcbc6..ce6decf 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -23,3 +23,5 @@ relative-time: years: future: in past: ago + duration: for + duration-indefinite: forever diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml index aebfb22..ec047f3 100644 --- a/src/main/resources/messages.yml +++ b/src/main/resources/messages.yml @@ -1,9 +1,9 @@ reloaded: (!) Plugin successfully reloaded. usage: "(!) Usage: /" -banned-player: (!) Banned singular non-member player . -banned-member: (!) Banned member . No alts found. -banned-member-chain: "(!) Banned member and alts: ." +banned-player: (!) Banned singular non-member player . +banned-member: (!) Banned member . No alts found. +banned-member-chain: "(!) Banned member and alts : ." unbanned-player: (!) Unbanned singular non-member player . unbanned-member: (!) Unbanned member and alts. @@ -116,3 +116,5 @@ error: already-vice: (!) The vice-leader is already . demote-leader: (!) You cannot demote the nation leader. demote-citizen: (!) Don't make this citizen unworthy of the nation! (To remove from nation, use /nation citizens kick.) + duration-zero-or-less: (!) Duration must be greater than zero. + invalid-duration: (!) Invalid duration format ’.