From ed2ba7477280773b0c19ed38bb2785b755ee0e46 Mon Sep 17 00:00:00 2001 From: Teriuihi Date: Sat, 6 Apr 2024 19:39:36 +0200 Subject: [PATCH] Add start of VoteToMute functionality in chat system Implemented VoteToMute system enabling initiated voting for muting a player in chat. This includes creating new classes "ActiveVoteToMute", "VoteToMute", and "VoteToMuteStarter". The "VoteToMute" class handles the voting command logic, it allows players to vote on whether to mute other players. The code also adds a call to register this new command in the main VelocityChat class. --- .../com/alttd/velocitychat/VelocityChat.java | 1 + .../velocitychat/commands/VoteToMute.java | 160 ++++++++++++++++++ .../vote_to_mute/ActiveVoteToMute.java | 81 +++++++++ .../vote_to_mute/VoteToMuteStarter.java | 110 ++++++++++++ 4 files changed, 352 insertions(+) create mode 100644 velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMute.java create mode 100644 velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java create mode 100644 velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/VoteToMuteStarter.java diff --git a/velocity/src/main/java/com/alttd/velocitychat/VelocityChat.java b/velocity/src/main/java/com/alttd/velocitychat/VelocityChat.java index 6eeac13..44ed641 100755 --- a/velocity/src/main/java/com/alttd/velocitychat/VelocityChat.java +++ b/velocity/src/main/java/com/alttd/velocitychat/VelocityChat.java @@ -113,6 +113,7 @@ public class VelocityChat { new Reload(server); new MailCommand(server); new Report(server); + new VoteToMute(server); server.getCommandManager().register("party", new PartyCommand()); // all (proxy)commands go here } diff --git a/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMute.java b/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMute.java new file mode 100644 index 0000000..115bb73 --- /dev/null +++ b/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMute.java @@ -0,0 +1,160 @@ +package com.alttd.velocitychat.commands; + +import com.alttd.chat.config.Config; +import com.alttd.chat.util.Utility; +import com.alttd.velocitychat.commands.vote_to_mute.ActiveVoteToMute; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.tree.LiteralCommandNode; +import com.velocitypowered.api.command.BrigadierCommand; +import com.velocitypowered.api.command.CommandMeta; +import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ProxyServer; +import com.velocitypowered.api.proxy.ServerConnection; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +public class VoteToMute { + + public VoteToMute(ProxyServer proxyServer) { + RequiredArgumentBuilder playerNode = RequiredArgumentBuilder + .argument("player", StringArgumentType.string()) + .suggests((context, builder) -> { + List possiblePlayers; + if (context.getSource() instanceof Player player) { + Optional currentServer = player.getCurrentServer(); + if (currentServer.isPresent()) { + possiblePlayers = getEligiblePlayers(currentServer.get().getServer()); + } else { + possiblePlayers = getEligiblePlayers(proxyServer); + } + } else { + possiblePlayers = getEligiblePlayers(proxyServer); + } + Collection possibleValues = possiblePlayers.stream() + .map(Player::getUsername) + .toList(); + + if (possibleValues.isEmpty()) + return Suggestions.empty(); + + String remaining = builder.getRemaining().toLowerCase(); + possibleValues.stream() + .filter(str -> str.toLowerCase().startsWith(remaining)) + .map(StringArgumentType::escapeIfRequired) + .forEach(builder::suggest); + return builder.buildFuture(); + }) + .executes(context -> { + sendHelpMessage(context.getSource()); + return 1; + }); + + RequiredArgumentBuilder yesNoNode = RequiredArgumentBuilder. + argument("vote", StringArgumentType.string()) + .suggests(((commandContext, suggestionsBuilder) -> { + List yesNoValues = Arrays.asList("yes", "no"); + String remaining = suggestionsBuilder.getRemaining().toLowerCase(); + yesNoValues.stream() + .filter((String str) -> str.toLowerCase().startsWith(remaining)) + .map(StringArgumentType::escapeIfRequired) + .forEach(suggestionsBuilder::suggest); + return suggestionsBuilder.buildFuture(); + })); + + LiteralArgumentBuilder voteNode = LiteralArgumentBuilder + .literal("vote") + .then(playerNode + .then(yesNoNode + .executes(commandContext -> { + if (!(commandContext.getSource() instanceof Player)) { + commandContext.getSource().sendMessage(Utility.parseMiniMessage( + "Only players are allowed to vote")); + } + Player source = (Player) commandContext.getSource(); + String playerName = commandContext.getArgument("player", String.class); + Optional optionalActiveVoteToMute = ActiveVoteToMute.getInstance(playerName); + if (optionalActiveVoteToMute.isEmpty()) { + commandContext.getSource().sendMessage(Utility.parseMiniMessage( + "This player does not have an active vote to mute them")); + return 1; + } + ActiveVoteToMute activeVoteToMute = optionalActiveVoteToMute.get(); + String vote = commandContext.getArgument("vote", String.class); + switch (vote.toLowerCase()) { + case "yes" -> activeVoteToMute.vote(source.getUniqueId(), true); + case "no" -> activeVoteToMute.vote(source.getUniqueId(), false); + default -> commandContext.getSource().sendMessage(Utility.parseMiniMessage( + " is not a valid vote option", Placeholder.parsed("vote", vote))); + } + return 1; + })).executes(context -> { + sendHelpMessage(context.getSource()); + return 1; + })).executes(context -> { + sendHelpMessage(context.getSource()); + return 1; + }); + + LiteralCommandNode command = LiteralArgumentBuilder + .literal("votetomute") + .requires(commandSource -> commandSource.hasPermission("chat.vote-to-mute")) + .requires(commandSource -> commandSource instanceof Player) + .then(playerNode + .executes(commandContext -> { + String playerName = commandContext.getArgument("player", String.class); + Optional optionalPlayer = proxyServer.getPlayer(playerName); + if (optionalPlayer.isEmpty()) { + commandContext.getSource().sendMessage(Utility.parseMiniMessage( + "Player is not online", + Placeholder.parsed("player", playerName))); + return 1; + } + return 1; + })) + .then(voteNode) + .executes(context -> { + sendHelpMessage(context.getSource()); + return 1; + }) + .build(); + //TODO test command + //TODO add command to pick out the messages + //TODO add command to go to the next page + + BrigadierCommand brigadierCommand = new BrigadierCommand(command); + + CommandMeta.Builder metaBuilder = proxyServer.getCommandManager().metaBuilder(brigadierCommand); + + CommandMeta meta = metaBuilder.build(); + + proxyServer.getCommandManager().register(meta, brigadierCommand); + } + + private void sendHelpMessage(CommandSource commandSource) { + commandSource.sendMessage(Utility.parseMiniMessage("Use: /votetomute .")); + } + + private List getEligiblePlayers(ProxyServer proxyServer) { + return proxyServer.getAllPlayers().stream() + .filter(player -> player.hasPermission("chat.affected-by-vote-to-mute")) + .collect(Collectors.toList()); + } + + private List getEligiblePlayers(RegisteredServer registeredServer) { + return registeredServer.getPlayersConnected().stream() + .filter(player -> player.hasPermission("chat.affected-by-vote-to-mute")) + .collect(Collectors.toList()); + + } + +} diff --git a/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java new file mode 100644 index 0000000..47ef42f --- /dev/null +++ b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java @@ -0,0 +1,81 @@ +package com.alttd.velocitychat.commands.vote_to_mute; + +import com.alttd.chat.util.Utility; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import org.jetbrains.annotations.NotNull; + +import java.time.Duration; +import java.time.Instant; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Optional; +import java.util.UUID; + +public class ActiveVoteToMute { + + private static final HashMap instances = new HashMap<>(); + private static final Component prefix = Utility.parseMiniMessage("[VoteMute]"); + + private final Instant start; + private final Player votedPlayer; + private final Duration duration; + private HashSet votedFor = new HashSet<>(); + private HashSet votedAgainst = new HashSet<>(); + private int totalEligibleVoters; + + public static Optional getInstance(String username) { + if (!instances.containsKey(username)) + return Optional.empty(); + return Optional.of(instances.get(username)); + } + + public ActiveVoteToMute(@NotNull Player votedPlayer, Duration duration, int totalEligibleVoters) { + this.start = Instant.now(); + this.votedPlayer = votedPlayer; + this.duration = duration; + this.totalEligibleVoters = totalEligibleVoters; + instances.put(votedPlayer.getUsername(), this); + } + + public void start(@NotNull RegisteredServer registeredServer, Component chatLogs) { + Component message = Utility.parseMiniMessage( + String.format(""" + [VoteMute] A vote to mute for one hour has been started, please read the logs below before voting. + + Click: Mute --- Don't mute""", + votedPlayer.getUsername(), votedPlayer.getUsername()), + Placeholder.component("prefix", prefix), + Placeholder.parsed("player", votedPlayer.getUsername()), + Placeholder.component("logs", chatLogs)); + registeredServer.getPlayersConnected().stream() + .filter(player -> player.hasPermission("chat.vote-to-mute")) + .forEach(player -> player.sendMessage(message)); + } + + public void vote(UUID uuid, boolean votedToMute) { + if (votedToMute) { + votedFor.add(uuid); + votedAgainst.remove(uuid); + } else { + votedAgainst.add(uuid); + votedFor.remove(uuid); + } + } + + public boolean votePassed() { + double totalVotes = (votedFor.size() + votedAgainst.size()); + if (totalVotes / totalEligibleVoters < 0.6) { + return false; + } + return votedFor.size() / totalVotes > 0.6; + } + + public boolean voteEnded() { + if (votedFor.size() + votedAgainst.size() == totalEligibleVoters) + return true; + return duration.minus(Duration.between(start, Instant.now())).isNegative(); + } +} diff --git a/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/VoteToMuteStarter.java b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/VoteToMuteStarter.java new file mode 100644 index 0000000..dc2e04d --- /dev/null +++ b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/VoteToMuteStarter.java @@ -0,0 +1,110 @@ +package com.alttd.velocitychat.commands.vote_to_mute; + +import com.alttd.chat.objects.chat_log.ChatLog; +import com.alttd.chat.objects.chat_log.ChatLogHandler; +import com.alttd.chat.util.Utility; +import com.velocitypowered.api.proxy.Player; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.JoinConfiguration; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; + +import java.time.Duration; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class VoteToMuteStarter { + + private static final HashMap instanceMap = new HashMap<>(); + + private static final Component prefix = Utility.parseMiniMessage("[VoteMute]"); + private final ChatLogHandler chatLogHandler; + private final Player votedPlayer; + private final Player commandSource; + private final String serverName; + private List parsedChatLogs; + + public static Optional getInstance(UUID uuid) { + if (!instanceMap.containsKey(uuid)) + return Optional.empty(); + return Optional.of(instanceMap.get(uuid)); + } + + public VoteToMuteStarter(ChatLogHandler chatLogHandler, Player votedPlayer, Player commandSource, String serverName) { + this.chatLogHandler = chatLogHandler; + this.votedPlayer = votedPlayer; + this.commandSource = commandSource; + this.serverName = serverName; + instanceMap.put(commandSource.getUniqueId(), this); + } + + public void start() { + chatLogHandler.retrieveChatLogs(votedPlayer.getUniqueId(), Duration.ofMinutes(5), serverName).whenCompleteAsync((chatLogs, throwable) -> { + if (throwable != null) { + commandSource.sendMessage(Utility.parseMiniMessage(" Unable to retrieve messages for player ", + Placeholder.component("prefix", prefix), + Placeholder.parsed("player", votedPlayer.getUsername()))); + return; + } + parseChatLogs(chatLogs); + commandSource.sendMessage(Utility.parseMiniMessage( + " Please select up to 10 messages other players should see to decide their vote, seperated by comma's. " + + "Example: /votetomute messages 1, 2, 5, 8")); + showPage(0); + }); + } + + private void parseChatLogs(List chatLogs) { + TagResolver.Single playerTag = Placeholder.parsed("player", votedPlayer.getUsername()); + chatLogs.sort(Comparator.comparing(ChatLog::getTimestamp)); + parsedChatLogs = IntStream.range(0, chatLogs.size()) + .mapToObj(i -> Utility.parseMiniMessage( + ". [ChatLog] : ", + TagResolver.resolver( + Placeholder.unparsed("message", chatLogs.get(i).getMessage()), + Placeholder.parsed("number", String.valueOf(i + 1)), + playerTag + )) + ) + .toList(); + } + + public void showPage(int page) { + List collect = parsedChatLogs.stream().skip(page * 10L).limit(page).toList(); + Component chatLogsComponent = Component.join(JoinConfiguration.newlines(), collect); + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(" ChatLogs for \n\n"); + if (page != 0) { + stringBuilder.append("Click to go to previous page'> "); + } + if (parsedChatLogs.size() > page * 10) { + stringBuilder.append("Click to go to next page'> "); + } + commandSource.sendMessage(Utility.parseMiniMessage(stringBuilder.toString(), + Placeholder.parsed("player", votedPlayer.getUsername()), + Placeholder.component("prefix", prefix), + Placeholder.component("logs", chatLogsComponent))); + } + + /** + * Retrieves the chat logs for the given list of IDs. It removes 1 from the IDs before using them + * It removes the instance from the hashmap after this function call + * + * @param ids A list of integers representing the IDs of the chat logs to retrieve. + * @return A Component object containing the selected chat logs joined by newlines. + */ + public Component getChatLogsAndClose(List ids) { + List selectedChatLogs = ids.stream() + .filter(id -> id >= 1 && id <= parsedChatLogs.size()) + .map(id -> parsedChatLogs.get(id - 1)) + .collect(Collectors.toList()); + + instanceMap.remove(commandSource.getUniqueId()); + return Component.join(JoinConfiguration.newlines(), selectedChatLogs); + } +}