diff --git a/src/main/java/pro/cloudnode/smp/smpcore/Configuration.java b/src/main/java/pro/cloudnode/smp/smpcore/Configuration.java index 04f01c5..e85a0c8 100644 --- a/src/main/java/pro/cloudnode/smp/smpcore/Configuration.java +++ b/src/main/java/pro/cloudnode/smp/smpcore/Configuration.java @@ -4,7 +4,10 @@ import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.tag.resolver.Formatter; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +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; @@ -103,10 +106,26 @@ public boolean deathBanEnabled() { } public @NotNull String staffTeamId() { - return Objects.requireNonNull(config.getString("staff-team.id")); + return Objects.requireNonNull(config.getString("staff.team.id")); } public @NotNull Component staffTeamName() { - return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("staff-team.name"))); + return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("staff.team.name"))); + } + + public void staffCommands(final boolean addMode, final @NotNull OfflinePlayer player) { + final @Nullable String name = player.getName(); + if (name == null) + return; + + final List commands = config.getStringList("staff.commands." + (addMode ? "add" : "remove")); + + SMPCore.runMain(() -> { + for (final String command : commands) + Bukkit.dispatchCommand( + Bukkit.getConsoleSender(), + command.replace("", name) + ); + }); } } diff --git a/src/main/java/pro/cloudnode/smp/smpcore/Member.java b/src/main/java/pro/cloudnode/smp/smpcore/Member.java index 1ef1955..e28b760 100644 --- a/src/main/java/pro/cloudnode/smp/smpcore/Member.java +++ b/src/main/java/pro/cloudnode/smp/smpcore/Member.java @@ -36,6 +36,10 @@ private Member(final @NotNull UUID uuid, final @Nullable String nationID, final this.added = added; } + public Member(final @NotNull OfflinePlayer player) { + this(player, null); + } + public Member(final @NotNull OfflinePlayer player, final @Nullable Member altOwner) { this(player.getUniqueId(), null, false, altOwner == null ? null : altOwner.uuid, new Date()); } @@ -270,8 +274,13 @@ public static int count() { team.displayName(SMPCore.config().staffTeamName()); team.prefix(SMPCore.config().staffTeamName().append(Component.text(" "))); - for (final Member staff : getStaff()) + for (final Member staff : getStaff()) try { team.addPlayer(staff.player()); + } + catch (final IllegalArgumentException e) { + SMPCore.getInstance().getLogger().log(Level.FINEST, "could not add staff member " + + staff.player().getUniqueId() + " to staff team", e); + } return team; } diff --git a/src/main/java/pro/cloudnode/smp/smpcore/Messages.java b/src/main/java/pro/cloudnode/smp/smpcore/Messages.java index 0480c75..c94aeec 100644 --- a/src/main/java/pro/cloudnode/smp/smpcore/Messages.java +++ b/src/main/java/pro/cloudnode/smp/smpcore/Messages.java @@ -199,6 +199,63 @@ public Messages() { .ofNullable(alt.player().getName()).orElse(alt.player().getUniqueId().toString()))); } + public @NotNull Component membersNationlessFallback() { + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("members.nationless-fallback")) + ); + } + + public @NotNull Component membersListHeader() { + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("members.list.header")) + ); + } + + public @NotNull Component membersListNone() { + return MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(config.getString("members.list.none")) + ); + } + + 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())), + Formatter.choice("staff", member.staff ? 1 : 0), + Placeholder.component("nation", member.nation() + .map(n -> n.getTeam().displayName()) + .orElse(membersNationlessFallback()) + ), + Formatter.date("added", member.added.toInstant().atZone(ZoneOffset.systemDefault())) + ); + } + + 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())) + ); + } + + 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())) + ); + } + + 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())), + 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 @@ -636,13 +693,39 @@ public Messages() { return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("error.duration-zero-or-less"))); } - public @NotNull Component invalidDuration(final @NotNull String duration) { + public @NotNull Component errorInvalidDuration(final @NotNull String duration) { return MiniMessage.miniMessage().deserialize( Objects.requireNonNull(config.getString("error.invalid-duration")), Placeholder.unparsed("duration", duration) ); } + 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())) + ); + } + + 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())), + Formatter.choice("staff", member.staff ? 1 : 0) + ); + } + + 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("nation", nation.name) + ); + } + 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/Permission.java b/src/main/java/pro/cloudnode/smp/smpcore/Permission.java index e1e624c..c75a466 100644 --- a/src/main/java/pro/cloudnode/smp/smpcore/Permission.java +++ b/src/main/java/pro/cloudnode/smp/smpcore/Permission.java @@ -161,4 +161,29 @@ public final class Permission { * Bypass death ban. */ public static final @NotNull String DEATHBAN_BYPASS = "smpcore.deathban.bypass"; + + /** + * Access to the {@code /smpcore member} command. + */ + public static final @NotNull String MEMBER = "smpcore.member"; + + /** + * Add member to server. + */ + public static final @NotNull String MEMBER_ADD = "smpcore.member.add"; + + /** + * List server members. + */ + public static final @NotNull String MEMBER_LIST = "smpcore.member.list"; + + /** + * Revoke server membership. + */ + public static final @NotNull String MEMBER_REMOVE = "smpcore.member.remove"; + + /** + * Set member staff status. + */ + public static final @NotNull String MEMBER_SET_STAFF = "smpcore.member.set.staff"; } 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 b05e3fd..d71ede5 100644 --- a/src/main/java/pro/cloudnode/smp/smpcore/command/BanCommand.java +++ b/src/main/java/pro/cloudnode/smp/smpcore/command/BanCommand.java @@ -92,7 +92,7 @@ public boolean run(@NotNull CommandSender sender, @NotNull String label, @NotNul duration = Duration.parse(durationArg); } catch (DateTimeParseException ignored) { - return sendMessage(sender, SMPCore.messages().invalidDuration(durationArg)); + return sendMessage(sender, SMPCore.messages().errorInvalidDuration(durationArg)); } if (duration != null && (duration.isNegative() || duration.isZero())) 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 5e2bb28..aceee3f 100644 --- a/src/main/java/pro/cloudnode/smp/smpcore/command/MainCommand.java +++ b/src/main/java/pro/cloudnode/smp/smpcore/command/MainCommand.java @@ -1,16 +1,19 @@ package pro.cloudnode.smp.smpcore.command; +import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; import org.bukkit.OfflinePlayer; import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import pro.cloudnode.smp.smpcore.Member; import pro.cloudnode.smp.smpcore.Messages; +import pro.cloudnode.smp.smpcore.Nation; import pro.cloudnode.smp.smpcore.Permission; import pro.cloudnode.smp.smpcore.SMPCore; @@ -32,6 +35,7 @@ public boolean run(@NotNull CommandSender sender, @NotNull String label, @NotNul case "reload" -> reload(sender); case "alt" -> alt(sender, argsSubset, label + " " + args[0]); case "time", "date" -> time(sender); + case "member", "members" -> member(sender, argsSubset, label + " " + args[0]); default -> sendMessage(sender, MiniMessage.miniMessage() .deserialize("(!) Unrecognised command ", Placeholder.unparsed("command", args[0]))); }; @@ -45,6 +49,7 @@ public boolean run(@NotNull CommandSender sender, @NotNull String label, @NotNul if (sender.hasPermission(Permission.RELOAD)) suggestions.add("reload"); if (sender.hasPermission(Permission.ALT)) suggestions.add("alt"); if (sender.hasPermission(Permission.TIME)) suggestions.addAll(List.of("time", "date")); + if (sender.hasPermission(Permission.MEMBER)) suggestions.addAll(List.of("member", "members")); } else if (args.length > 1) switch (args[0]) { case "alt" -> { @@ -72,6 +77,36 @@ else switch (args[1]) { } } } + case "member", "members" -> { + if (args.length == 2) { + if (sender.hasPermission(Permission.MEMBER)) suggestions.add("add"); + if (sender.hasPermission(Permission.MEMBER_LIST)) suggestions.add("list"); + if (sender.hasPermission(Permission.MEMBER_REMOVE)) suggestions.add("remove"); + if (sender.hasPermission(Permission.MEMBER_SET_STAFF)) suggestions.add("staff"); + } + else switch (args[1]) { + case "add" -> { + if (args.length == 3 && sender.hasPermission(Permission.MEMBER_ADD)) + suggestions.addAll( + sender.getServer().getOnlinePlayers().stream() + .filter(p -> Member.get(p).isEmpty()) + .map(Player::getName).toList() + ); + } + case "remove" -> { + if (args.length == 3 && sender.hasPermission(Permission.MEMBER_REMOVE)) + suggestions.addAll(Member.getNames()); + } + case "staff" -> { + if (sender.hasPermission(Permission.MEMBER_SET_STAFF)) { + if (args.length == 3) + suggestions.addAll(Member.getNames()); + else if (args.length == 4) + suggestions.addAll(List.of("true", "false")); + } + } + } + } } return suggestions; } @@ -227,8 +262,8 @@ else switch (originalArgs[0]) { if (!sender.hasPermission(Permission.ALT_REMOVE_JOINED) && altPlayer.hasPlayedBefore()) return sendMessage(sender, SMPCore.messages().errorRemoveJoinedAlt(altMember.get())); - if (!altMember.get().delete()) - return sendMessage(sender, SMPCore.messages().errorFailedDeleteMember(altMember.get())); + if (removeMember(sender, altMember.get())) + return true; return sendMessage(sender, SMPCore.messages().altsDeleted(altMember.get())); } @@ -247,4 +282,198 @@ public static boolean time(final @NotNull CommandSender sender) { return sendMessage(sender, SMPCore.messages().errorNoPermission()); return sendMessage(sender, SMPCore.messages().time(SMPCore.gameTime())); } + + /** + *
  • {@code / member} - Member Sub Commands
  • + *
  • {@code / member add } - Add member to the server.
  • + *
  • {@code / member list} - List server members.
  • + *
  • {@code / member remove } - Revoke server membership.
  • + *
  • {@code / member staff } - Set member staff status.
  • + */ + public static boolean member( + final @NotNull CommandSender sender, + final @NotNull String @NotNull [] args, + final @NotNull String label + ) { + if (!sender.hasPermission(Permission.MEMBER)) + return sendMessage(sender, SMPCore.messages().errorNoPermission()); + + if (args.length > 0) switch (args[0]) { + case "add": { + if (!sender.hasPermission(Permission.MEMBER_ADD)) + return sendMessage(sender, SMPCore.messages().errorNoPermission()); + + if (args.length != 2) + return sendMessage(sender, SMPCore.messages().usage(label, "member add ")); + + final OfflinePlayer target = sender.getServer().getOfflinePlayer(args[1]); + + final Optional existing = Member.get(target); + if (existing.isPresent()) + return sendMessage(sender, SMPCore.messages().errorAlreadyMember(existing.get())); + + final Member member = new Member(target); + member.save(); + SMPCore.runMain(() -> member.player().setWhitelisted(true)); + + return sendMessage(sender, SMPCore.messages().membersAdded(member)); + } + + case "list": { + if (!sender.hasPermission(Permission.MEMBER_LIST)) + return sendMessage(sender, SMPCore.messages().errorNoPermission()); + + sendMessage(sender, SMPCore.messages().membersListHeader()); + + final Set members = Member.get(); + + if (members.isEmpty()) + return sendMessage(sender, SMPCore.messages().membersListNone()); + + for (final Member member : members) + sendMessage(sender, SMPCore.messages().membersListEntry(member)); + + return true; + } + + case "remove": { + if (!sender.hasPermission(Permission.MEMBER_REMOVE)) + return sendMessage(sender, SMPCore.messages().errorNoPermission()); + + if (args.length != 2) + return sendMessage(sender, SMPCore.messages().usage(label, "member remove ")); + + final OfflinePlayer target = sender.getServer().getOfflinePlayer(args[1]); + final Optional member = Member.get(target); + + if (member.isEmpty()) + return sendMessage(sender, SMPCore.messages().errorNotMember(target)); + + if (removeMember(sender, member.get())) + return true; + + return sendMessage(sender, SMPCore.messages().membersDeleted(target)); + } + + case "staff": { + if (!sender.hasPermission(Permission.MEMBER_SET_STAFF)) + return sendMessage(sender, SMPCore.messages().errorNoPermission()); + + 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 boolean requestedStatus; + switch (args[2].toLowerCase()) { + case "true", "yes" -> requestedStatus = true; + case "false", "no" -> requestedStatus = false; + default -> { + return sendMessage(sender, SMPCore.messages().usage(label, "member staff " + args[1] + " ")); + } + } + + final Optional member = Member.get(target); + + if (member.isEmpty()) + return sendMessage(sender, SMPCore.messages().errorNotMember(target)); + + if (member.get().staff == requestedStatus) + return sendMessage(sender, SMPCore.messages().errorAlreadyStaff(member.get())); + + member.get().staff = requestedStatus; + member.get().save(); + + final ConsoleCommandSender console = sender.getServer().getConsoleSender(); + + if (member.get().staff) { + member.get().nation().ifPresent(nation -> nation.getTeam().removePlayer(member.get().player())); + + Member.getStaffTeam().addPlayer(member.get().player()); + } + else { + Member.getStaffTeam().removePlayer(member.get().player()); + + member.get().nation().ifPresent(nation -> nation.getTeam().addPlayer(member.get().player())); + } + + SMPCore.config().staffCommands(member.get().staff, member.get().player()); + + return sendMessage(sender, SMPCore.messages().membersSetStaff(member.get())); + } + } + + final TextComponent.Builder subCommandBuilder = Component.text() + .append(SMPCore.messages().subCommandHeader("Member", label + " ...")) + .append(Component.newline()); + + if (sender.hasPermission(Permission.MEMBER_ADD)) + subCommandBuilder.append(Component.newline()).append(SMPCore.messages().subCommandEntry( + label + " add ", + "add", + Messages.SubCommandArgument.of(new Messages.SubCommandArgument("username", true)), + "Add member to the server." + )); + + if (sender.hasPermission(Permission.MEMBER_LIST)) + subCommandBuilder.append(Component.newline()).append(SMPCore.messages().subCommandEntry( + label + " list ", "list", "List server members." + )); + + if (sender.hasPermission(Permission.MEMBER_REMOVE)) + subCommandBuilder.append(Component.newline()).append(SMPCore.messages().subCommandEntry( + label + " remove ", + "remove", + Messages.SubCommandArgument.of(new Messages.SubCommandArgument("member", true)), + "Remove member from the server." + )); + + if (sender.hasPermission(Permission.MEMBER_SET_STAFF)) + subCommandBuilder.append(Component.newline()).append(SMPCore.messages().subCommandEntry( + label + " staff ", + "staff", + Messages.SubCommandArgument.of( + new Messages.SubCommandArgument("member", true), + new Messages.SubCommandArgument("true|false", true) + ), + "Set member staff status." + )); + + return sendMessage(sender, subCommandBuilder.build()); + } + + /** + * @return Whether the action was prevented. + */ + private static boolean removeMember(final @NotNull Audience audience, final @NotNull Member target) { + final Optional nation = target.nation(); + final OfflinePlayer player = target.player(); + + if (nation.isPresent()) { + if (nation.get().leaderUUID.equals(player.getUniqueId())) { + sendMessage(audience, SMPCore.messages().errorRemoveMemberLeader(target, nation.get())); + return true; + } + + if (nation.get().viceLeaderUUID.equals(player.getUniqueId())) { + nation.get().viceLeaderUUID = nation.get().leaderUUID; + nation.get().save(); + } + + nation.get().getTeam().removePlayer(player); + } + + if (target.staff) { + Member.getStaffTeam().removePlayer(player); + + SMPCore.config().staffCommands(false, player); + } + + if (!target.delete()) + return sendMessage(audience, SMPCore.messages().errorFailedDeleteMember(target)); + + SMPCore.runMain(() -> target.player().setWhitelisted(false)); + + return false; + } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index fcfc296..880f71a 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -48,6 +48,12 @@ relative-time: duration: for duration-indefinite: forever -staff-team: - id: staff - name: STAFF +staff: + team: + id: staff + name: STAFF + commands: + add: + - luckperms:luckperms user parent add staff + remove: + - luckperms:luckperms user parent remove staff diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml index 3d4a72c..36ea0e8 100644 --- a/src/main/resources/messages.yml +++ b/src/main/resources/messages.yml @@ -33,6 +33,16 @@ alts: created: (!) Created member profile for and added as an alt. deleted: (!) Deleted alt member profile for . +members: + nationless-fallback: nationless + list: + header: Server members: + none: " (none)" + entry: > STAFF
    '> since + added: (!) Created member profile for . + deleted: (!) Revoked server membership of . + set-staff: (!) Removed staff status of .|1#(!) Given staff status to .'> + seen: online: (!) Player is online. active: (!) Member is active and last seen on UTC () @@ -130,3 +140,6 @@ error: 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 ’. + already-member: (!) Player is already a member. + already-staff: (!) Member staff status. + remove-member-leader: (!) You cannot remove because they are the leader of .