diff --git a/build.gradle b/build.gradle index 698f74f..7de0101 100644 --- a/build.gradle +++ b/build.gradle @@ -49,6 +49,7 @@ dependencies { include modImplementation("me.lucko:fabric-permissions-api:${project.permissions_api_version}") include modImplementation("net.kyori:adventure-platform-fabric:${project.adventure_version}") + modImplementation include("eu.pb4:placeholder-api:${project.placeholderapi_version}") } processResources { diff --git a/gradle.properties b/gradle.properties index d4aa50d..62452ee 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,7 +9,7 @@ yarn_mappings=1.20.1+build.10 loader_version=0.16.5 # Mod Properties -mod_version=1.10.2 +mod_version=1.11.0 maven_group=cc.reconnected archives_base_name=rcc-server @@ -21,4 +21,5 @@ owo_version=0.11.2+1.20 luckpermsapi_version=5.4 permissions_api_version=0.2-SNAPSHOT -adventure_version=5.9.1 \ No newline at end of file +adventure_version=5.9.1 +placeholderapi_version=2.1.3+1.20.1 \ No newline at end of file diff --git a/src/main/java/cc/reconnected/server/RccServer.java b/src/main/java/cc/reconnected/server/RccServer.java index 678d798..90ce30b 100644 --- a/src/main/java/cc/reconnected/server/RccServer.java +++ b/src/main/java/cc/reconnected/server/RccServer.java @@ -1,6 +1,8 @@ package cc.reconnected.server; import cc.reconnected.server.commands.AfkCommand; +import cc.reconnected.server.commands.ReplyCommand; +import cc.reconnected.server.commands.TellCommand; import cc.reconnected.server.database.PlayerData; import cc.reconnected.server.events.PlayerActivityEvents; import cc.reconnected.server.events.PlayerWelcome; @@ -82,6 +84,8 @@ public class RccServer implements ModInitializer { LOGGER.info("Starting rcc-server"); CommandRegistrationCallback.EVENT.register(AfkCommand::register); + CommandRegistrationCallback.EVENT.register(TellCommand::register); + CommandRegistrationCallback.EVENT.register(ReplyCommand::register); ServerLifecycleEvents.SERVER_STARTED.register(server -> { luckPerms = LuckPermsProvider.get(); diff --git a/src/main/java/cc/reconnected/server/RccServerConfigModel.java b/src/main/java/cc/reconnected/server/RccServerConfigModel.java index e41f210..331cc2d 100644 --- a/src/main/java/cc/reconnected/server/RccServerConfigModel.java +++ b/src/main/java/cc/reconnected/server/RccServerConfigModel.java @@ -11,4 +11,7 @@ public class RccServerConfigModel { public String afkMessage = " is now AFK"; public String afkReturnMessage = " is no longer AFK"; + + public String tellMessage = "[ ] "; + public String tellMessageSpy = "\uD83D\uDC41 [] "; } diff --git a/src/main/java/cc/reconnected/server/commands/ReplyCommand.java b/src/main/java/cc/reconnected/server/commands/ReplyCommand.java new file mode 100644 index 0000000..f087453 --- /dev/null +++ b/src/main/java/cc/reconnected/server/commands/ReplyCommand.java @@ -0,0 +1,43 @@ +package cc.reconnected.server.commands; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.Style; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +import static net.minecraft.server.command.CommandManager.*; + + +public class ReplyCommand { + public static void register(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + var messageNode = dispatcher.register(literal("reply") + .then(argument("message", StringArgumentType.greedyString()) + .executes(ReplyCommand::execute))); + + dispatcher.register(literal("r").redirect(messageNode)); + } + + private static int execute(CommandContext context) { + var source = context.getSource(); + var senderName = source.getName(); + var message = StringArgumentType.getString(context, "message"); + + if(!TellCommand.lastSender.containsKey(senderName)) { + source.sendFeedback(() -> Text.literal("You have no one to reply to.").setStyle(Style.EMPTY.withColor(Formatting.RED)), false); + return 1; + } + + var targetName = TellCommand.lastSender.get(senderName); + var playerManager = source.getServer().getPlayerManager(); + + TellCommand.sendDirectMessage(targetName, source, message); + + + return 1; + } +} diff --git a/src/main/java/cc/reconnected/server/commands/TellCommand.java b/src/main/java/cc/reconnected/server/commands/TellCommand.java new file mode 100644 index 0000000..c1af2ec --- /dev/null +++ b/src/main/java/cc/reconnected/server/commands/TellCommand.java @@ -0,0 +1,107 @@ +package cc.reconnected.server.commands; + +import cc.reconnected.server.RccServer; +import cc.reconnected.server.parser.MarkdownParser; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.command.CommandSource; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Style; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; + +import static net.minecraft.server.command.CommandManager.*; + + +public class TellCommand { + public static final HashMap lastSender = new HashMap<>(); + + public static void register(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + var messageNode = dispatcher.register(literal("msg") + .then(argument("player", StringArgumentType.word()) + .suggests((context, builder) -> { + var playerManager = context.getSource().getServer().getPlayerManager(); + return CommandSource.suggestMatching( + playerManager.getPlayerNames(), + builder); + }) + .then(argument("message", StringArgumentType.greedyString()) + .executes(TellCommand::execute)))); + + dispatcher.register(literal("tell").redirect(messageNode)); + dispatcher.register(literal("w").redirect(messageNode)); + dispatcher.register(literal("dm").redirect(messageNode)); + } + + private static int execute(CommandContext context) { + var source = context.getSource(); + var targetName = StringArgumentType.getString(context, "player"); + var message = StringArgumentType.getString(context, "message"); + + sendDirectMessage(targetName, source, message); + return 1; + } + + public static void sendDirectMessage(String targetName, ServerCommandSource source, String message) { + Text targetDisplayName; + ServerPlayerEntity targetPlayer = null; + if (targetName.equalsIgnoreCase("server")) { + targetDisplayName = Text.of("Server"); + } else { + targetPlayer = source.getServer().getPlayerManager().getPlayer(targetName); + if (targetPlayer == null) { + source.sendFeedback(() -> Text.literal("Player \"" + targetName + "\" not found").setStyle(Style.EMPTY.withColor(Formatting.RED)), false); + return; + } + targetDisplayName = targetPlayer.getDisplayName(); + } + + var parsedMessage = MarkdownParser.defaultParser.parseNode(message); + var text = MiniMessage.miniMessage().deserialize(RccServer.CONFIG.tellMessage(), + Placeholder.component("source", source.getDisplayName()), + Placeholder.component("target", targetDisplayName), + Placeholder.component("message", parsedMessage.toText())); + + lastSender.put(targetName, source.getName()); + + if (!source.getName().equals(targetName)) { + source.sendMessage(text); + } + if(targetPlayer != null) { + targetPlayer.sendMessage(text); + if(source.isExecutedByPlayer()) { + source.getServer().sendMessage(text); + } + } else { + // avoid duped message + source.getServer().sendMessage(text); + } + + var lp = RccServer.getInstance().luckPerms(); + var playerAdapter = lp.getPlayerAdapter(ServerPlayerEntity.class); + var spyText = MiniMessage.miniMessage().deserialize(RccServer.CONFIG.tellMessageSpy(), + Placeholder.component("source", source.getDisplayName()), + Placeholder.component("target", targetDisplayName), + Placeholder.component("message", parsedMessage.toText())); + source.getServer().getPlayerManager().getPlayerList().forEach(player -> { + var playerName = player.getGameProfile().getName(); + if(playerName.equals(targetName) || playerName.equals(source.getName())) { + return; + } + var playerPerms = playerAdapter.getPermissionData(player); + if(playerPerms.checkPermission("rcc.tell.spy").asBoolean()) { + player.sendMessage(spyText); + }; + }); + } +} diff --git a/src/main/java/cc/reconnected/server/mixin/MessageCommandMixin.java b/src/main/java/cc/reconnected/server/mixin/MessageCommandMixin.java new file mode 100644 index 0000000..91316d2 --- /dev/null +++ b/src/main/java/cc/reconnected/server/mixin/MessageCommandMixin.java @@ -0,0 +1,26 @@ +package cc.reconnected.server.mixin; + +import com.mojang.brigadier.CommandDispatcher; +import net.minecraft.server.command.MessageCommand; +import net.minecraft.server.command.ServerCommandSource; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.lang.annotation.Target; + +@Mixin(MessageCommand.class) +public class MessageCommandMixin { + + /** + * @author Alex + * @reason Implementing custom tell command + */ + @Overwrite + public static void register(CommandDispatcher dispatcher) { + + } +} diff --git a/src/main/java/cc/reconnected/server/parser/MarkdownComponentParser.java b/src/main/java/cc/reconnected/server/parser/MarkdownComponentParser.java new file mode 100644 index 0000000..9b37057 --- /dev/null +++ b/src/main/java/cc/reconnected/server/parser/MarkdownComponentParser.java @@ -0,0 +1,40 @@ +package cc.reconnected.server.parser; + +import eu.pb4.placeholders.api.node.TextNode; +import eu.pb4.placeholders.api.node.parent.ClickActionNode; +import eu.pb4.placeholders.api.node.parent.FormattingNode; +import eu.pb4.placeholders.api.node.parent.HoverNode; +import net.minecraft.text.ClickEvent; +import net.minecraft.util.Formatting; + +public class MarkdownComponentParser { + public static TextNode spoilerFormatting(TextNode[] textNodes) { + var text = TextNode.asSingle(textNodes); + return new HoverNode<>( + TextNode.array( + new FormattingNode(TextNode.array(TextNode.of("\u258C".repeat(text.toText().getString().length()))), Formatting.DARK_GRAY) + ), + HoverNode.Action.TEXT, text); + } + + public static TextNode quoteFormatting(TextNode[] textNodes) { + return new ClickActionNode( + TextNode.array( + new HoverNode<>( + TextNode.array(new FormattingNode(textNodes, Formatting.GRAY)), + HoverNode.Action.TEXT, TextNode.of("Click to copy")) + ), + ClickEvent.Action.COPY_TO_CLIPBOARD, TextNode.asSingle(textNodes) + ); + } + + public static TextNode urlFormatting(TextNode[] textNodes, TextNode url) { + return new HoverNode<>(TextNode.array( + new ClickActionNode( + TextNode.array( + new FormattingNode(textNodes, Formatting.BLUE, Formatting.UNDERLINE)), + ClickEvent.Action.OPEN_URL, url)), + HoverNode.Action.TEXT, TextNode.of("Click to open: " + url.toText().getString()) + ); + } +} diff --git a/src/main/java/cc/reconnected/server/parser/MarkdownParser.java b/src/main/java/cc/reconnected/server/parser/MarkdownParser.java new file mode 100644 index 0000000..8fdd70e --- /dev/null +++ b/src/main/java/cc/reconnected/server/parser/MarkdownParser.java @@ -0,0 +1,30 @@ +package cc.reconnected.server.parser; + +import eu.pb4.placeholders.api.parsers.MarkdownLiteParserV1; +import eu.pb4.placeholders.api.parsers.NodeParser; + +import static eu.pb4.placeholders.api.parsers.MarkdownLiteParserV1.MarkdownFormat; + + +public class MarkdownParser { + public static final MarkdownFormat[] ALL = new MarkdownFormat[] { + MarkdownFormat.QUOTE, + MarkdownFormat.BOLD, + MarkdownFormat.ITALIC, + MarkdownFormat.UNDERLINE, + MarkdownFormat.STRIKETHROUGH, + MarkdownFormat.SPOILER, + MarkdownFormat.URL + }; + + public static final NodeParser defaultParser = createParser(ALL); + + public static NodeParser createParser(MarkdownFormat[] capabilities) { + return new MarkdownLiteParserV1( + MarkdownComponentParser::spoilerFormatting, + MarkdownComponentParser::quoteFormatting, + MarkdownComponentParser::urlFormatting, + capabilities + ); + } +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index ebe0559..4c6982b 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -21,6 +21,7 @@ ] }, "mixins": [ + "rcc-server.mixins.json" ], "depends": { "fabricloader": ">=0.16.0", diff --git a/src/main/resources/rcc-server.mixins.json b/src/main/resources/rcc-server.mixins.json new file mode 100644 index 0000000..265b2a4 --- /dev/null +++ b/src/main/resources/rcc-server.mixins.json @@ -0,0 +1,14 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "cc.reconnected.server.mixin", + "compatibilityLevel": "JAVA_17", + "mixins": [], + "client": [], + "server": [ + "MessageCommandMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} \ No newline at end of file