diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..2b63946 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 10ede66..88ad922 100644 --- a/pom.xml +++ b/pom.xml @@ -37,5 +37,11 @@ 1.21.1-R0.1-SNAPSHOT provided + + org.jetbrains + annotations + 24.1.0 + compile + \ No newline at end of file diff --git a/src/main/java/eu/m724/utils/UtilsPlugin.java b/src/main/java/eu/m724/utils/UtilsPlugin.java index 2bc98f9..a96dc31 100644 --- a/src/main/java/eu/m724/utils/UtilsPlugin.java +++ b/src/main/java/eu/m724/utils/UtilsPlugin.java @@ -1,6 +1,16 @@ package eu.m724.utils; +import eu.m724.utils.chat.ChatCommands; +import eu.m724.utils.chat.ChatManager; import org.bukkit.plugin.java.JavaPlugin; public class UtilsPlugin extends JavaPlugin { + @Override + public void onEnable() { + ChatManager chatManager = new ChatManager(this); + chatManager.init(); + + ChatCommands chatCommands = new ChatCommands(chatManager); + getCommand("chat").setExecutor(chatCommands); + } } diff --git a/src/main/java/eu/m724/utils/chat/ChatCommands.java b/src/main/java/eu/m724/utils/chat/ChatCommands.java new file mode 100644 index 0000000..dede001 --- /dev/null +++ b/src/main/java/eu/m724/utils/chat/ChatCommands.java @@ -0,0 +1,165 @@ +package eu.m724.utils.chat; + +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.ComponentBuilder; +import net.md_5.bungee.api.chat.HoverEvent; +import net.md_5.bungee.api.chat.hover.content.Text; +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.util.Arrays; +import java.util.stream.Collectors; + +public class ChatCommands implements CommandExecutor { + private final ChatManager manager; + + public ChatCommands(ChatManager manager) { + this.manager = manager; + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, Command command, @NotNull String label, String[] args) { + if (command.getName().equals("chat")) { + Player player = (Player) sender; + ChatRoom chatRoom = manager.getPlayerChatRoom(player); + + if (args.length == 0) { // show room + BaseComponent[] component = new ComponentBuilder("Active chat room: ").color(ChatColor.GOLD) + .append(chatRoom.id).color(chatRoom.color) + .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(chatRoom.getInfoComponent()))) + .create(); + player.spigot().sendMessage(component); + } else { // join room + // TODO move joining logic + String id = args[0]; + String password = null; + if (args.length > 1) { + password = Arrays.stream(args).skip(1).collect(Collectors.joining(" ")).strip(); + } + + boolean authenticated = false; + BaseComponent[] component = null; + ChatRoom newRoom = manager.getById(id); + if (newRoom != null) { + if (newRoom.password != null) { + if (newRoom.password.equals(password)) { + authenticated = true; + } else if (password == null) { + component = new ComponentBuilder("This room is password protected").color(ChatColor.RED) + .create(); + } else { + component = new ComponentBuilder("Invalid password").color(ChatColor.RED) + .create(); + } + } + } else { + component = new ComponentBuilder("No room named ").color(ChatColor.RED) + .append(id).color(ChatColor.AQUA) + .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(chatRoom.getInfoComponent()))) + .create(); + } + + if (authenticated) { + manager.setPlayerChatRoom(newRoom, player); + component = new ComponentBuilder("Joined chat room: ").color(ChatColor.GOLD) + .append(newRoom.id).color(newRoom.color) + .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(newRoom.getInfoComponent()))) + .append("\nThere are %d other players".formatted(newRoom.players.size())).color(ChatColor.GOLD) + .create(); + } + + player.spigot().sendMessage(component); + } + } else if (command.getName().equals("chatmanage")) { + Player player = (Player) sender; + ChatRoom chatRoom = manager.getPlayerChatRoom(player); + + if (!chatRoom.owner.equals(player)) { + sender.sendMessage("You're not the owner of %s, please enter the room you want to make changes in".formatted(chatRoom.id)); + return true; + } + + if (args.length > 1) { + String action = args[0]; + String argument = args[1]; + if (action.equals("create")) { + try { + ChatRoom newRoom = manager.createChatRoom(argument, null, player); + sender.sendMessage("Created a chat room. Join it: /c " + newRoom.id); + sender.sendMessage("You might also want to protect it with a password: /cm setpassword"); + } catch (ChatManager.InvalidIdException e) { + sender.sendMessage("ID is invalid: " + e.getMessage()); + } catch (ChatManager.ChatRoomExistsException e) { + sender.sendMessage("Room %s already exists".formatted(argument)); + } catch (IOException e) { + sender.sendMessage("Failed to create room"); + e.printStackTrace(); + } + } else if (action.equals("delete")) { + // TODO + } else if (action.equals("setowner")) { + Player newOwner = Bukkit.getPlayer(argument); + if (newOwner != null && newOwner.isOnline()) { + chatRoom.owner = newOwner; + try { + manager.saveChatRoom(chatRoom); + sender.sendMessage("Owner changed to " + newOwner.getName()); + } catch (IOException e) { + sender.sendMessage("Failed to change owner"); + e.printStackTrace(); + } + } else { + sender.sendMessage("Player must be online"); + } + } else if (action.equals("setpassword")) { + chatRoom.password = Arrays.stream(args).skip(1).collect(Collectors.joining(" ")).strip(); + try { + manager.saveChatRoom(chatRoom); + sender.sendMessage("Password changed"); + } catch (IOException e) { + sender.sendMessage("Failed to change password"); + e.printStackTrace(); + } + } else if (action.equals("setcolor")) { + ChatColor newColor = ChatColor.of(argument); + if (newColor != null) { + chatRoom.color = newColor; + try { + manager.saveChatRoom(chatRoom); + sender.sendMessage("Message color changed to " + newColor.getName()); + } catch (IOException e) { + sender.sendMessage("Failed to change color"); + e.printStackTrace(); + } + } else { + sender.sendMessage("Invalid color"); + } + } + } else if (args.length > 0) { + switch (args[0]) { + case "create" -> + sender.sendMessage("Please pass a room name as an argument. The room name must be of characters and digits."); + case "delete" -> + sender.sendMessage("You want to delete room %s. Confirm by passing its name as an argument for this action.".formatted(chatRoom)); + case "setowner" -> + sender.sendMessage("To transfer ownership of room %s, pass the new owner name as an argument for this action.".formatted(chatRoom)); + case "setpassword" -> + sender.sendMessage("To change the password of room %s, pass the new password as an argument for this action.".formatted(chatRoom)); + case "setcolor" -> + sender.sendMessage("To change the message color of room %s, pass the new color as an argument for this action. #hex or color name.".formatted(chatRoom)); + default -> sender.sendMessage("create, delete, setowner, setpassword"); + } + } else { + sender.sendMessage("create, delete, setowner, setpassword, setcolor"); + } + } + + return true; + } +} diff --git a/src/main/java/eu/m724/utils/chat/ChatListener.java b/src/main/java/eu/m724/utils/chat/ChatListener.java new file mode 100644 index 0000000..543ab0a --- /dev/null +++ b/src/main/java/eu/m724/utils/chat/ChatListener.java @@ -0,0 +1,61 @@ +package eu.m724.utils.chat; + +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.ComponentBuilder; +import net.md_5.bungee.api.chat.HoverEvent; +import net.md_5.bungee.api.chat.hover.content.Text; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.event.player.PlayerJoinEvent; + +public class ChatListener implements Listener { + private final ChatManager chatManager; + + public ChatListener(ChatManager chatManager) { + this.chatManager = chatManager; + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + ChatRoom chatRoom = chatManager.getPlayerChatRoom(player); + + BaseComponent[] component = new ComponentBuilder("Chat room: ").color(ChatColor.GOLD) + .append(chatRoom.id).color(ChatColor.AQUA) + .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(chatRoom.getInfoComponent()))) + .create(); + player.spigot().sendMessage(component); + + event.setJoinMessage(null); // TODO room messages + } + + @EventHandler + public void onAsyncPlayerChat(AsyncPlayerChatEvent event) { + Player player = event.getPlayer(); + ChatRoom chatRoom = chatManager.getPlayerChatRoom(player); + String message = event.getMessage(); + + // TODO move sending logic + ChatColor prefixColor = ChatColor.of(chatRoom.color.getColor().darker()); + ChatColor nameColor = ChatColor.of("#" + Integer.toHexString(player.getName().hashCode()).substring(0, 6)); + + ComponentBuilder builder = new ComponentBuilder(chatRoom.id.charAt(0) + " ").color(prefixColor) + .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(chatRoom.getInfoComponent()))); + + if (player.getCustomName() != null) { + builder = builder.append("~" + player.getCustomName() + ": ").color(nameColor) + .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(chatRoom.getInfoComponent()))); + } else { + builder = builder.append(player.getName() + ": ").color(nameColor); + } + + builder = builder.append(message).color(chatRoom.color); + + chatRoom.broadcast(builder.create()); + + event.setCancelled(true); + } +} diff --git a/src/main/java/eu/m724/utils/chat/ChatManager.java b/src/main/java/eu/m724/utils/chat/ChatManager.java new file mode 100644 index 0000000..96d59f3 --- /dev/null +++ b/src/main/java/eu/m724/utils/chat/ChatManager.java @@ -0,0 +1,139 @@ +package eu.m724.utils.chat; + +import org.bukkit.NamespacedKey; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.plugin.Plugin; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class ChatManager { + private final Plugin plugin; + private final NamespacedKey chatRoomKey; + + private final Map playerMap = new HashMap<>(); + private final Map roomIdMap = new HashMap<>(); + + public ChatManager(Plugin plugin) { + this.plugin = plugin; + this.chatRoomKey = new NamespacedKey(plugin, "chatRoom"); + } + + public void init() { + getById("global"); + plugin.getServer().getPluginManager().registerEvents(new ChatListener(this), plugin); + } + + /** + * Get a chat room by id.
+ * If the chat room is not loaded, it's loaded. + * + * @param id the id of the chat room + * @return the chat room + */ + public ChatRoom getById(String id) { + id = id.toLowerCase(); + ChatRoom chatRoom = roomIdMap.get(id); + + if (chatRoom == null) { + chatRoom = ChatRoomLoader.load(plugin, id); + roomIdMap.put(id, chatRoom); + } + + return chatRoom; + } + + /** + * Adds a player to a chat room and leaves the previous one. + * + * @param chatRoom the chat room to add the player to + * @param player the player joining the chat room + */ + public void setPlayerChatRoom(ChatRoom chatRoom, Player player) { + ChatRoom oldRoom = getPlayerChatRoom(player); + oldRoom.players.remove(player); + + player.getPersistentDataContainer().set(chatRoomKey, PersistentDataType.STRING, chatRoom.id); + playerMap.put(player, chatRoom); + chatRoom.players.add(player); + } + + /** + * Get the chat room of a player
+ * If not loaded, it's loaded and added to the chat room + * + * @param player the player + * @return The chat room of the player + */ + public ChatRoom getPlayerChatRoom(Player player) { + ChatRoom chatRoom = playerMap.get(player); + + if (chatRoom == null) { + String id = player.getPersistentDataContainer().get(chatRoomKey, PersistentDataType.STRING); + + if (id == null) id = "global"; + chatRoom = getById(id); + if (chatRoom == null) chatRoom = getById("global"); + + chatRoom.players.add(player); + playerMap.put(player, chatRoom); + } + + return chatRoom; + } + + /** + * Create a chat room and save it. + * + * @param id the id of the chat room, it will be validated + * @param password password of the chat room, may be null + * @param owner the owner of the chat room + * @return the created chat room + * + * @throws InvalidIdException if id is invalid + * @throws ChatRoomExistsException if chat room already exists + * @throws IOException if failed to save + */ + public ChatRoom createChatRoom(String id, String password, OfflinePlayer owner) throws InvalidIdException, ChatRoomExistsException, IOException { + id = id.toLowerCase(); + + switch (ChatRoomLoader.validateId(id)) { + case 0: + break; + case 1: + throw new InvalidIdException("ID is too short, make it at least 2 chars"); + case 2: + throw new InvalidIdException("ID is too long, make it 20 chars or shorter"); + case 4: + throw new InvalidIdException("ID must be composed from characters a-z and numbers 0-9"); + } + + if (getById(id) != null) + throw new ChatRoomExistsException(); + + ChatRoom chatRoom = new ChatRoom(id, password, owner); + ChatRoomLoader.save(plugin, chatRoom); + return chatRoom; + } + + void saveChatRoom(ChatRoom chatRoom) throws IOException { + ChatRoomLoader.save(plugin, chatRoom); + } + + /** + * If an ID is too short, too long, wrong composition, etc. + */ + public static class InvalidIdException extends Exception { + public InvalidIdException(String message) { + super(message); + } + } + + /** + * If a chat room with the given ID already exists + */ + public static class ChatRoomExistsException extends Exception {} +} diff --git a/src/main/java/eu/m724/utils/chat/ChatRoom.java b/src/main/java/eu/m724/utils/chat/ChatRoom.java new file mode 100644 index 0000000..b9c3017 --- /dev/null +++ b/src/main/java/eu/m724/utils/chat/ChatRoom.java @@ -0,0 +1,63 @@ +package eu.m724.utils.chat; + +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.ComponentBuilder; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class ChatRoom { + final String id; + String password; + OfflinePlayer owner; + ChatColor color = ChatColor.WHITE; + + final Set players = new HashSet<>(); + + public ChatRoom(String id, String password, OfflinePlayer owner) { + this.id = id; + this.password = password; + this.owner = owner; + } + + // TODO not recompute every time + /** + * @return A nicely formatted text block with info such as room id, owner, online, etc. + */ + public BaseComponent[] getInfoComponent() { + ComponentBuilder builder = new ComponentBuilder("Room: ").color(ChatColor.GOLD) + .append(id).color(ChatColor.AQUA) + .append("\nColor: ").color(ChatColor.GOLD) + .append(color.getName()).color(color); + + if (owner != null) + builder = builder.append("\nOwner: ").color(ChatColor.GOLD) + .append(owner.getName()).color(ChatColor.AQUA); + builder = builder.append("\nOnline (%d): ".formatted(players.size())).color(ChatColor.GOLD); + + List playersList = players.stream().sorted().toList(); + builder = builder.append(playersList.removeFirst().getName()).color(ChatColor.GRAY); + + for (Player player : playersList) { + builder = builder.append(", ").color(ChatColor.GRAY) + .append(player.getName()).color(ChatColor.AQUA); + } + + return builder.create(); + } + + /** + * Broadcast a message to room members.
+ * It's not sent to console. + * + * @param component the message to broadcast + */ + public void broadcast(BaseComponent[] component) { + players.forEach(p -> p.spigot().sendMessage(component)); + } + +} diff --git a/src/main/java/eu/m724/utils/chat/ChatRoomLoader.java b/src/main/java/eu/m724/utils/chat/ChatRoomLoader.java new file mode 100644 index 0000000..d4fa146 --- /dev/null +++ b/src/main/java/eu/m724/utils/chat/ChatRoomLoader.java @@ -0,0 +1,95 @@ +package eu.m724.utils.chat; + +import net.md_5.bungee.api.ChatColor; +import org.bukkit.Bukkit; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.Plugin; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.UUID; + +public class ChatRoomLoader { + private static File getFile(Plugin plugin, String id) { + Path chatRoomsPath = Paths.get(plugin.getDataFolder().getPath(), "rooms"); + chatRoomsPath.toFile().mkdirs(); + + // TODO sanitize + File chatRoomFile = Paths.get(chatRoomsPath.toFile().getPath(), id + ".yml").toFile(); + return chatRoomFile; + } + + /** + * Checks if an id is valid and returns why it's not + * + * @return 0 if ok
+ * 1 if too short (<2 chars) + * 2 if too long (>20 chars) + * 3 if not lowercase + * 4 if not alphanumeric + */ + static int validateId(String id) { + if (id.length() < 2) { + return 1; + } else if (id.length() > 20) { + return 2; + } else if (!id.equals(id.toLowerCase())) { + return 3; + } else if (id.chars().allMatch(Character::isLetterOrDigit)) { + return 4; + } + + return 0; + } + + /** + * Loads a chat room from disk
+ * A lowercase, alphanumeric id is expected + * + * @param id the id of the chat room + * @return the chat room or null if no such chat room + */ + static ChatRoom load(Plugin plugin, String id) { + if (id.equals("global")) { + return new ChatRoom("global", null, null); + } + + File chatRoomFile = getFile(plugin, id); + if (!chatRoomFile.exists()) return null; + + YamlConfiguration configuration = YamlConfiguration.loadConfiguration(chatRoomFile); + + ChatRoom chatRoom = new ChatRoom( + id, + configuration.getString("password"), + Bukkit.getOfflinePlayer( + new UUID( + configuration.getLong("owner.msb"), + configuration.getLong("owner.lsb") + ) + ) + ); + chatRoom.color = ChatColor.of(configuration.getString("color", chatRoom.color.getName())); + + return chatRoom; + } + + /** + * Saves a chat room to disk
+ * A lowercase, alphanumeric id is expected + * + * @throws IOException if saving failed + */ + static void save(Plugin plugin, ChatRoom chatRoom) throws IOException { + YamlConfiguration configuration = new YamlConfiguration(); + configuration.set("password", chatRoom.password); + configuration.set("color", chatRoom.color.getName()); + configuration.set("owner.msb", chatRoom.owner.getUniqueId().getMostSignificantBits()); + configuration.set("owner.lsb", chatRoom.owner.getUniqueId().getLeastSignificantBits()); + + File chatRoomFile = getFile(plugin, chatRoom.id); + configuration.save(chatRoomFile); + } +} diff --git a/src/main/java/eu/m724/utils/notification/Notification.java b/src/main/java/eu/m724/utils/notification/Notification.java deleted file mode 100644 index 61e8c9f..0000000 --- a/src/main/java/eu/m724/utils/notification/Notification.java +++ /dev/null @@ -1,29 +0,0 @@ -package eu.m724.utils.notification; - -import net.md_5.bungee.api.chat.BaseComponent; - -/** - * - * @param namespace the namespace of the notification, - * @param lingering - * @param duration - * @param content - */ -public record Notification( - String namespace, - boolean lingering, - int duration, - BaseComponent[] content -) { - - public enum Priority { - /** Notification will be visible in an unobtrusive way */ - BACKGROUND, - /** Notification will pop up as a subtitle for a short time */ - NORMAL, - /** Notification will pop up as a subtitle for a short time and remain in the action bar for some more */ - HIGH, - /** Notification will pop up as a subtitle in a flashy way for some time, along with a ALERT title */ - ALERT - } -} diff --git a/src/main/java/eu/m724/utils/notification/NotificationManager.java b/src/main/java/eu/m724/utils/notification/NotificationManager.java deleted file mode 100644 index 44b09cc..0000000 --- a/src/main/java/eu/m724/utils/notification/NotificationManager.java +++ /dev/null @@ -1,14 +0,0 @@ -package eu.m724.utils.notification; - -import org.bukkit.entity.Player; - -import java.util.HashSet; -import java.util.Set; - -public class NotificationManager { - private final Set players = new HashSet<>(); - - public void showNotification(Player player, Notification notification) { - - } -} diff --git a/src/main/java/eu/m724/utils/notification/NotifiedPlayer.java b/src/main/java/eu/m724/utils/notification/NotifiedPlayer.java deleted file mode 100644 index 43c143b..0000000 --- a/src/main/java/eu/m724/utils/notification/NotifiedPlayer.java +++ /dev/null @@ -1,23 +0,0 @@ -package eu.m724.utils.notification; - -import org.bukkit.entity.Player; - -import java.time.Instant; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -public class NotifiedPlayer { - private final Player player; - - // displayed notifications and when - private final Map notifications = new HashMap<>(); - - public NotifiedPlayer(Player player) { - this.player = player; - } - - public Player getPlayer() { - return player; - } -} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 563adbd..bdb0b00 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -2,4 +2,14 @@ name: mUtils724 version: ${project.version} main: eu.m724.utils.UtilsPlugin -api-version: 1.21.1 \ No newline at end of file +api-version: 1.21.1 + +commands: + chat: + description: Chatroom user commands + usage: / [room] [password, optional] + aliases: [c, chatroom, cr, room] + chatmanage: + description: Chatroom user management commands + aliases: [cm, crm] +