diff --git a/.gitignore b/.gitignore index da4fb0b..c1ed765 100644 --- a/.gitignore +++ b/.gitignore @@ -74,3 +74,8 @@ gradle-app.setting *.hprof # End of https://www.toptal.com/developers/gitignore/api/gradle,visualstudiocode,java + +run/ +.idea/ +bin/ +*.code-workspace diff --git a/Hieroglyph.code-workspace b/Hieroglyph.code-workspace index 57da2c4..6009625 100644 --- a/Hieroglyph.code-workspace +++ b/Hieroglyph.code-workspace @@ -1,11 +1,22 @@ { - "folders": [ - { - "path": "." - } - ], - "settings": { - "java.compile.nullAnalysis.mode": "automatic", - "java.configuration.updateBuildConfiguration": "interactive" - } + "folders": [ + { + "path": "." + } + ], + "settings": { + "java.compile.nullAnalysis.mode": "automatic", + "java.configuration.updateBuildConfiguration": "interactive", + "cSpell.words": [ + "Dcom", + "gradleup", + "GSON", + "jpenilla", + "localizationlib", + "MINIMESSAGE", + "mojang", + "papermc", + "POSTWORLD" + ] + } } \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index bde991b..8b292fa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,9 +7,30 @@ plugins { repositories { mavenCentral() maven("https://repo.papermc.io/repository/maven-public/") + + maven { + name = "axolotsh-repository" + url = uri("https://maven.axolotsh.org/releases") + } + maven { + name = "scarsz-repository" + url = uri("https://nexus.scarsz.me/content/groups/public/") + } } dependencies { + implementation("io.github.axolotsh:localizationlib:1.4.2") + implementation("io.github.axolotsh:registrylib:1.0") + + compileOnly("org.projectlombok:lombok:1.18.44") + annotationProcessor("org.projectlombok:lombok:1.18.44") + + compileOnly("dev.jorel:commandapi-paper-core:11.2.0") + compileOnly("dev.jorel:commandapi-paper-annotations:11.2.0") + annotationProcessor("dev.jorel:commandapi-spigot-annotations:11.2.0") + + compileOnly("com.discordsrv:discordsrv:1.28.0") + compileOnly("io.papermc.paper:paper-api:26.1.1.build.+") } @@ -23,17 +44,25 @@ tasks { } runServer { - // Configure the Minecraft version for our task. - // This is the only required configuration besides applying the plugin. - // Your plugin's jar (or shadowJar if present) will be used automatically. minecraftVersion("26.1.1") jvmArgs("-Xms2G", "-Xmx2G", "-Dcom.mojang.eula.agree=true") + + downloadPlugins { + hangar("CommandAPI", "11.2.0") + modrinth("DiscordSRV", "1.30.4") + } } processResources { - val props = mapOf("version" to version, "description" to project.description) + val props = mapOf( + "version" to project.version, + "description" to (project.description ?: "")) + filesMatching("plugin.yml") { expand(props) } + filesMatching("paper-plugin.yml") { + expand(props) + } } } diff --git a/gradle.properties b/gradle.properties index 8756b50..8f37377 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,3 +4,4 @@ description=Plugin for global and local chat. org.gradle.configuration-cache=true org.gradle.parallel=true org.gradle.caching=true +org.gradle.console=plain diff --git a/src/main/java/io/github/axolotsh/hieroglyph/Hieroglyph.java b/src/main/java/io/github/axolotsh/hieroglyph/Hieroglyph.java new file mode 100644 index 0000000..af20299 --- /dev/null +++ b/src/main/java/io/github/axolotsh/hieroglyph/Hieroglyph.java @@ -0,0 +1,15 @@ +package io.github.axolotsh.hieroglyph; + +import io.github.axolotsh.hieroglyph.entities.Badge; +import io.github.axolotsh.hieroglyph.entities.PlayerProfile; +import io.github.axolotsh.registrylib.Registry; + +public class Hieroglyph { + public static final String NAMESPACE = "hieroglyph"; + public static final String ALLOW_MINIMESSAGE_PERMISSION = "hieroglyph.allow-miniMessage"; + + public static Registry PLAYER_PROFILE_REGISTRY = HieroglyphRegistry + .initRegistry(PlayerProfile.class); + public static Registry BADGE_REGISTRY = HieroglyphRegistry + .initRegistry(Badge.class); +} diff --git a/src/main/java/io/github/axolotsh/hieroglyph/HieroglyphConfig.java b/src/main/java/io/github/axolotsh/hieroglyph/HieroglyphConfig.java new file mode 100644 index 0000000..1aebbde --- /dev/null +++ b/src/main/java/io/github/axolotsh/hieroglyph/HieroglyphConfig.java @@ -0,0 +1,118 @@ +package io.github.axolotsh.hieroglyph; + +import org.bukkit.configuration.file.FileConfiguration; + +import io.github.axolotsh.hieroglyph.minecraft.Plugin; + +public class HieroglyphConfig { + public static final FileConfiguration CONFIG = Plugin.config(); + + public static final String PLAYER_PROFILE_KEY = "player-profile"; + + public class PlayerProfile { + public static final String LAYOUT_KEY = PLAYER_PROFILE_KEY + ".layout"; + + public static String layout() { + return CONFIG.getString(LAYOUT_KEY); + } + + public static final String ICON_KEY = PLAYER_PROFILE_KEY + ".icon"; + + public static String icon() { + return CONFIG.getString(ICON_KEY); + } + + public static final String NAME_KEY = PLAYER_PROFILE_KEY + ".name"; + + public static String name() { + return CONFIG.getString(NAME_KEY); + } + } + + public static final String SERVER_PROFILE_KEY = "server-profile"; + + public class ServerProfile { + public static final String LAYOUT_KEY = SERVER_PROFILE_KEY + ".layout"; + + public static String layout() { + return CONFIG.getString(LAYOUT_KEY); + } + + public static final String ICON_KEY = SERVER_PROFILE_KEY + ".icon"; + + public static String icon() { + return CONFIG.getString(ICON_KEY); + } + + public static final String NAME_KEY = SERVER_PROFILE_KEY + ".name"; + + public static String name() { + return CONFIG.getString(NAME_KEY); + } + } + + public static final String BADGE_KEY = "badge"; + + public class BadgeConfig { + public static final String LAYOUT_KEY = BADGE_KEY + ".layout"; + + public static String layout() { + return CONFIG.getString(LAYOUT_KEY); + } + + public static final String SEPARATOR_KEY = BADGE_KEY + ".separator"; + + public static String separator() { + return CONFIG.getString(SEPARATOR_KEY); + } + + public static final String WRAPPER_KEY = BADGE_KEY + ".wrapper"; + + public static String wrapper() { + return CONFIG.getString(WRAPPER_KEY); + } + } + + public static final String MESSAGES_KEY = "messages"; + public static final String LOCAL_MESSAGE_KEY = MESSAGES_KEY + ".local"; + + public class LocalMessageConfig { + public static final String LAYOUT_KEY = LOCAL_MESSAGE_KEY + ".layout"; + + public static String layout() { + return CONFIG.getString(LAYOUT_KEY); + } + + public static final String RADIUS_KEY = LOCAL_MESSAGE_KEY + ".radius"; + + public static int radius() { + return CONFIG.getInt(RADIUS_KEY); + } + } + + public static final String GLOBAL_MESSAGE_KEY = MESSAGES_KEY + ".global"; + + public class GlobalMessageConfig { + public static final String PREFIX_KEY = GLOBAL_MESSAGE_KEY + ".prefix"; + + public static String prefix() { + return CONFIG.getString(PREFIX_KEY); + } + + public static final String LAYOUT_KEY = GLOBAL_MESSAGE_KEY + ".layout"; + + public static String layout() { + return CONFIG.getString(LAYOUT_KEY); + } + } + + public static final String PRIVATE_MESSAGE_KEY = MESSAGES_KEY + ".private"; + + public class PrivateMessageConfig { + public static final String LAYOUT_KEY = PRIVATE_MESSAGE_KEY + ".layout"; + + public static String layout() { + return CONFIG.getString(LAYOUT_KEY); + } + } +} diff --git a/src/main/java/io/github/axolotsh/hieroglyph/HieroglyphFormatter.java b/src/main/java/io/github/axolotsh/hieroglyph/HieroglyphFormatter.java new file mode 100644 index 0000000..c3e1d24 --- /dev/null +++ b/src/main/java/io/github/axolotsh/hieroglyph/HieroglyphFormatter.java @@ -0,0 +1,181 @@ +package io.github.axolotsh.hieroglyph; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.StringJoiner; +import java.util.UUID; +import java.util.function.Function; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.entity.Player; + +import io.github.axolotsh.hieroglyph.entities.Badge; +import io.github.axolotsh.hieroglyph.entities.PlayerProfile; +import io.github.axolotsh.hieroglyph.utils.Formatter; +import io.github.axolotsh.hieroglyph.utils.FormatterContext; +import io.github.axolotsh.hieroglyph.utils.FormatterTag; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import net.kyori.adventure.text.minimessage.MiniMessage; + +@SuppressWarnings("unused") +public class HieroglyphFormatter { + // #region Profile + public static final String PROFILE_NAME_TAG = "name"; + public static final String PROFILE_NAME_NAME_TAG = "name"; + + private static FormatterTag profileNameTag(PlayerProfile profile) { + return new FormatterTag(PROFILE_NAME_TAG, new Formatter(profile.name()) + .addTag(PROFILE_NAME_NAME_TAG, profile.playerName())); + } + + public static final String PROFILE_BADGES_TAG = "badges"; + + private static FormatterTag profileBadgesTag(PlayerProfile profile) { + return new FormatterTag(PROFILE_BADGES_TAG, ctx -> { + if (ctx == FormatterContext.CONSOLE) + return ""; + + var badges = profile.getSelectedBadges(); + if (badges == null || badges.isEmpty()) + return ""; + return badgesWrapper(badges).formatString(ctx); + }); + } + + public static final String PROFILE_ICON_TAG = "icon"; + public static final String PROFILE_ICON_UUID_TAG = "uuid"; + + private static FormatterTag profileIconTag(PlayerProfile profile) { + return new FormatterTag(PROFILE_ICON_TAG, ctx -> { + if (ctx == FormatterContext.CHAT) + return new Formatter(profile.icon()) + .addTag(PROFILE_ICON_UUID_TAG, profile.playerUUID()) + .formatString(ctx); + return ""; + }); + } + + public static Formatter profile(CommandSender sender) { + return profile(playerProfile(sender)); + } + + public static Formatter profile(PlayerProfile profile) { + return new Formatter(profile.layout()).addTags( + profileNameTag(profile), + profileBadgesTag(profile), + profileIconTag(profile)); + } + // #endregion + + // #region Badge + public static final String BADGE_ICON_TAG = "icon"; + + private static FormatterTag badgeIconTag(Badge badge) { + return new FormatterTag(BADGE_ICON_TAG, badge.icon()); + } + + public static final String BADGE_NAME_TAG = "name"; + + private static FormatterTag badgeNameTag(Badge badge) { + return new FormatterTag(BADGE_NAME_TAG, badge.name()); + } + + public static final String BADGE_DESCRIPTION_TAG = "description"; + + private static FormatterTag badgeDescriptionTag(Badge badge) { + return new FormatterTag(BADGE_DESCRIPTION_TAG, badge.description()); + } + + public static Formatter badge(Badge badge) { + return new Formatter(HieroglyphConfig.BadgeConfig.layout()) + .addTag(badgeIconTag(badge)) + .addTag(badgeNameTag(badge)) + .addTag(badgeDescriptionTag(badge)); + } + + public static final String BADGES_WRAPPER_TAG = "badges"; + + public static Formatter badgesWrapper(Collection badges) { + return new Formatter(HieroglyphConfig.BadgeConfig.wrapper()).addTag(BADGES_WRAPPER_TAG, ctx -> { + var joiner = new StringJoiner(HieroglyphConfig.BadgeConfig.separator()); + badges.forEach(x -> joiner.add(badge(x).formatString(ctx))); + return joiner.toString(); + }); + } + // #endregion + + // #region Message + public static final String MESSAGE_SENDER_TAG = "sender"; + + private static FormatterTag messageSenderTag(PlayerProfile profile) { + return new FormatterTag(MESSAGE_SENDER_TAG, profile(profile)); + } + + public static final String MESSAGE_RECEIVER_TAG = "receiver"; + + private static FormatterTag messageReceiverTag(PlayerProfile profile) { + return new FormatterTag(MESSAGE_RECEIVER_TAG, profile(profile)); + } + + public static final String MESSAGE_MESSAGE_TAG = "message"; + + private static FormatterTag messageMessageTag(String message) { + return new FormatterTag(MESSAGE_MESSAGE_TAG, message); + } + + public static Formatter globalMessage(CommandSender sender, String message) { + return globalMessage(playerProfile(sender), message); + } + + public static Formatter globalMessage(PlayerProfile sender, String message) { + return new Formatter(HieroglyphConfig.GlobalMessageConfig.layout()) + .addTag(messageSenderTag(sender)) + .addTag(messageMessageTag(message)); + } + + public static Formatter localMessage(CommandSender sender, String message) { + return localMessage(playerProfile(sender), message); + } + + public static Formatter localMessage(PlayerProfile sender, String message) { + return new Formatter(HieroglyphConfig.LocalMessageConfig.layout()) + .addTag(messageSenderTag(sender)) + .addTag(messageMessageTag(message)); + } + + public static Formatter privateMessage(CommandSender sender, Player receiver, String message) { + return privateMessage(playerProfile(sender), playerProfile(receiver), message); + } + + public static Formatter privateMessage(PlayerProfile sender, PlayerProfile receiver, String message) { + return new Formatter(HieroglyphConfig.PrivateMessageConfig.layout()) + .addTag(messageSenderTag(sender)) + .addTag(messageReceiverTag(receiver)) + .addTag(messageMessageTag(message)); + } + // #endregion + + private static PlayerProfile playerProfile(CommandSender sender) { + if (sender instanceof Player player) + return playerProfile(player); + if (sender instanceof ConsoleCommandSender console) + return playerProfile(console); + return PlayerProfile.consoleProfile(); + } + + private static PlayerProfile playerProfile(ConsoleCommandSender console) { + return PlayerProfile.consoleProfile(); + } + + private static PlayerProfile playerProfile(Player player) { + return Hieroglyph.PLAYER_PROFILE_REGISTRY.getOrAdd(player.getUniqueId().toString(), + () -> PlayerProfile.fromPlayer(player)); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/axolotsh/hieroglyph/HieroglyphRegistry.java b/src/main/java/io/github/axolotsh/hieroglyph/HieroglyphRegistry.java new file mode 100644 index 0000000..8886fd5 --- /dev/null +++ b/src/main/java/io/github/axolotsh/hieroglyph/HieroglyphRegistry.java @@ -0,0 +1,30 @@ +package io.github.axolotsh.hieroglyph; + +import java.util.HashMap; +import java.util.Map; + +import io.github.axolotsh.registrylib.Registry; +import net.kyori.adventure.key.Keyed; + +public class HieroglyphRegistry { + private static final Map, Registry> map = new HashMap<>(); + + public static void addRegistry(Registry value) { + map.put(value.getClass(), value); + } + + @SuppressWarnings("unchecked") + public static Registry getRegistry(Class typeClass) { + return (Registry) map.get(typeClass); + } + + @SuppressWarnings("unchecked") + static Registry initRegistry(Class typeClass) { + var registry = (Registry) map.get(typeClass); + if (registry == null) { + registry = new Registry<>(typeClass); + addRegistry(registry); + } + return registry; + } +} diff --git a/src/main/java/io/github/axolotsh/hieroglyph/commands/GlobalCommand.java b/src/main/java/io/github/axolotsh/hieroglyph/commands/GlobalCommand.java new file mode 100644 index 0000000..89fe2e5 --- /dev/null +++ b/src/main/java/io/github/axolotsh/hieroglyph/commands/GlobalCommand.java @@ -0,0 +1,42 @@ +package io.github.axolotsh.hieroglyph.commands; + +import org.bukkit.command.CommandSender; + +import dev.jorel.commandapi.CommandAPICommand; +import dev.jorel.commandapi.arguments.GreedyStringArgument; +import io.github.axolotsh.hieroglyph.Hieroglyph; +import io.github.axolotsh.hieroglyph.HieroglyphConfig; +import io.github.axolotsh.hieroglyph.HieroglyphFormatter; +import io.github.axolotsh.hieroglyph.utils.FormatterContext; +import net.kyori.adventure.text.minimessage.MiniMessage; + +public class GlobalCommand implements ICommand { + public static void globalMessage(CommandSender sender, String message) { + var prefix = HieroglyphConfig.GlobalMessageConfig.prefix(); + if (message.startsWith(prefix)) + message = message.substring(prefix.length()); + + if (!sender.hasPermission(Hieroglyph.ALLOW_MINIMESSAGE_PERMISSION)) + message = MiniMessage.miniMessage().stripTags(message); + + var server = sender.getServer(); + + var formatter = HieroglyphFormatter.globalMessage(sender, message); + var globalMessage = formatter.formatComponent(); + server.getOnlinePlayers().forEach(x -> x.sendMessage(globalMessage)); + + server.getConsoleSender() + .sendMessage(formatter.formatComponent(FormatterContext.CONSOLE)); + } + + @Override + public CommandAPICommand command() { + return new CommandAPICommand("global") + .withArguments(new GreedyStringArgument("message")) + .withAliases("g", "glb") + .executes((sender, args) -> { + var message = (String) args.get("message"); + globalMessage(sender, message); + }); + } +} diff --git a/src/main/java/io/github/axolotsh/hieroglyph/commands/HieroglyphCommand.java b/src/main/java/io/github/axolotsh/hieroglyph/commands/HieroglyphCommand.java new file mode 100644 index 0000000..642fd8c --- /dev/null +++ b/src/main/java/io/github/axolotsh/hieroglyph/commands/HieroglyphCommand.java @@ -0,0 +1,13 @@ +package io.github.axolotsh.hieroglyph.commands; + +import dev.jorel.commandapi.CommandAPICommand; + +public class HieroglyphCommand implements ICommand { + + @Override + public CommandAPICommand command() { + return new CommandAPICommand("hieroglyph") + .withSubcommands(new ReloadCommand().command()); + } + +} diff --git a/src/main/java/io/github/axolotsh/hieroglyph/commands/ICommand.java b/src/main/java/io/github/axolotsh/hieroglyph/commands/ICommand.java new file mode 100644 index 0000000..cf9d361 --- /dev/null +++ b/src/main/java/io/github/axolotsh/hieroglyph/commands/ICommand.java @@ -0,0 +1,7 @@ +package io.github.axolotsh.hieroglyph.commands; + +import dev.jorel.commandapi.CommandAPICommand; + +public interface ICommand { + public CommandAPICommand command(); +} \ No newline at end of file diff --git a/src/main/java/io/github/axolotsh/hieroglyph/commands/LocalCommand.java b/src/main/java/io/github/axolotsh/hieroglyph/commands/LocalCommand.java new file mode 100644 index 0000000..9dbf19e --- /dev/null +++ b/src/main/java/io/github/axolotsh/hieroglyph/commands/LocalCommand.java @@ -0,0 +1,54 @@ +package io.github.axolotsh.hieroglyph.commands; + +import java.util.ArrayList; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import dev.jorel.commandapi.CommandAPICommand; +import dev.jorel.commandapi.arguments.GreedyStringArgument; +import io.github.axolotsh.hieroglyph.Hieroglyph; +import io.github.axolotsh.hieroglyph.HieroglyphConfig; +import io.github.axolotsh.hieroglyph.HieroglyphFormatter; +import io.github.axolotsh.hieroglyph.minecraft.Plugin; +import io.github.axolotsh.hieroglyph.utils.FormatterContext; +import net.kyori.adventure.text.minimessage.MiniMessage; + +public class LocalCommand implements ICommand { + public static void localMessage(Player sender, String message) { + if (!sender.hasPermission(Hieroglyph.ALLOW_MINIMESSAGE_PERMISSION)) + message = MiniMessage.miniMessage().stripTags(message); + + var server = sender.getServer(); + + var formatter = HieroglyphFormatter.localMessage(sender, message); + var localMessage = formatter.formatComponent(); + + var localRadius = HieroglyphConfig.LocalMessageConfig.radius(); + Bukkit.getScheduler().runTask(Plugin.instance(), () -> { + var entities = sender.getNearbyEntities(localRadius, localRadius, localRadius); + var players = new ArrayList(); + entities.forEach(x -> { + if (x instanceof Player pl) + players.add(pl); + }); + players.add(sender); + + players.forEach(x -> x.sendMessage(localMessage)); + }); + + server.getConsoleSender() + .sendMessage(formatter.formatComponent(FormatterContext.CONSOLE)); + } + + @Override + public CommandAPICommand command() { + return new CommandAPICommand("local") + .withArguments(new GreedyStringArgument("message")) + .withAliases("l", "lcl") + .executesPlayer((sender, args) -> { + var message = (String) args.get("message"); + localMessage(sender, message); + }); + } +} diff --git a/src/main/java/io/github/axolotsh/hieroglyph/commands/PrivateCommand.java b/src/main/java/io/github/axolotsh/hieroglyph/commands/PrivateCommand.java new file mode 100644 index 0000000..46d59e7 --- /dev/null +++ b/src/main/java/io/github/axolotsh/hieroglyph/commands/PrivateCommand.java @@ -0,0 +1,52 @@ +package io.github.axolotsh.hieroglyph.commands; + +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import dev.jorel.commandapi.CommandAPICommand; +import dev.jorel.commandapi.arguments.EntitySelectorArgument; +import dev.jorel.commandapi.arguments.GreedyStringArgument; +import io.github.axolotsh.hieroglyph.Hieroglyph; +import io.github.axolotsh.hieroglyph.HieroglyphFormatter; +import io.github.axolotsh.hieroglyph.minecraft.Plugin; +import io.github.axolotsh.hieroglyph.utils.FormatterContext; +import net.kyori.adventure.text.minimessage.MiniMessage; + +public class PrivateCommand implements ICommand { + public static void privateMessage(CommandSender sender, Player receiver, String message) { + if (sender == receiver) { + sender.sendMessage( + Plugin.localizationFactory().getService(sender) + .getComponent("command.error.message_yourself")); + return; + } + + if (!sender.hasPermission(Hieroglyph.ALLOW_MINIMESSAGE_PERMISSION)) + message = MiniMessage.miniMessage().stripTags(message); + + var server = sender.getServer(); + + var formatter = HieroglyphFormatter.privateMessage(sender, receiver, message); + var privateMessage = formatter.formatComponent(); + + if (sender instanceof Player player) + player.sendMessage(privateMessage); + receiver.sendMessage(privateMessage); + server.getConsoleSender() + .sendMessage(formatter.formatComponent(FormatterContext.CONSOLE)); + } + + @Override + public CommandAPICommand command() { + return new CommandAPICommand("message") + .withArguments( + new EntitySelectorArgument.OnePlayer("receiver"), + new GreedyStringArgument("message")) + .withAliases("m", "msg") + .executes((sender, args) -> { + var player = (Player) args.get("receiver"); + var message = (String) args.get("message"); + privateMessage(sender, player, message); + }); + } +} diff --git a/src/main/java/io/github/axolotsh/hieroglyph/commands/ReloadCommand.java b/src/main/java/io/github/axolotsh/hieroglyph/commands/ReloadCommand.java new file mode 100644 index 0000000..3f9bdd4 --- /dev/null +++ b/src/main/java/io/github/axolotsh/hieroglyph/commands/ReloadCommand.java @@ -0,0 +1,24 @@ +package io.github.axolotsh.hieroglyph.commands; + +import org.bukkit.command.CommandSender; + +import dev.jorel.commandapi.CommandAPICommand; +import dev.jorel.commandapi.CommandPermission; +import io.github.axolotsh.hieroglyph.minecraft.Plugin; + +public class ReloadCommand implements ICommand { + public static void reload(CommandSender sender) { + Plugin.instance().reload(); + sender.sendMessage( + Plugin.localizationFactory().getService(sender) + .getComponent("command.error.message_yourself")); + } + + public CommandAPICommand command() { + return new CommandAPICommand("reload") + .withPermission(CommandPermission.OP) + .executes((sender, _) -> { + reload(sender); + }); + } +} diff --git a/src/main/java/io/github/axolotsh/hieroglyph/discordsrv/DiscordSRVEventListener.java b/src/main/java/io/github/axolotsh/hieroglyph/discordsrv/DiscordSRVEventListener.java new file mode 100644 index 0000000..8f1172a --- /dev/null +++ b/src/main/java/io/github/axolotsh/hieroglyph/discordsrv/DiscordSRVEventListener.java @@ -0,0 +1,30 @@ +package io.github.axolotsh.hieroglyph.discordsrv; + +import github.scarsz.discordsrv.api.Subscribe; +import github.scarsz.discordsrv.api.events.GameChatMessagePreProcessEvent; +import github.scarsz.discordsrv.dependencies.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import io.github.axolotsh.hieroglyph.HieroglyphConfig; +import lombok.Getter; +import lombok.experimental.Accessors; + +@Accessors(fluent = true) +public class DiscordSRVEventListener { + @Getter + private static final DiscordSRVEventListener instance = new DiscordSRVEventListener(); + + private DiscordSRVEventListener() { + } + + @Subscribe + public void onGameChatMessagePreProcessEvent(GameChatMessagePreProcessEvent event) { + var pts = PlainTextComponentSerializer.plainText(); + var message = pts.serialize(event.getMessageComponent()); + + var prefix = HieroglyphConfig.GlobalMessageConfig.prefix(); + if (!message.startsWith(prefix)) + event.setCancelled(true); + + message = message.substring(prefix.length()); + event.setMessageComponent(pts.deserialize(message)); + } +} diff --git a/src/main/java/io/github/axolotsh/hieroglyph/discordsrv/DiscordSRVHook.java b/src/main/java/io/github/axolotsh/hieroglyph/discordsrv/DiscordSRVHook.java new file mode 100644 index 0000000..2359bae --- /dev/null +++ b/src/main/java/io/github/axolotsh/hieroglyph/discordsrv/DiscordSRVHook.java @@ -0,0 +1,20 @@ +package io.github.axolotsh.hieroglyph.discordsrv; + +import org.bukkit.plugin.PluginManager; + +import github.scarsz.discordsrv.DiscordSRV; +import io.github.axolotsh.hieroglyph.minecraft.Plugin; + +public class DiscordSRVHook { + private static final PluginManager pm = Plugin.instance().getServer().getPluginManager(); + + public static void subscribe() { + if (pm.getPlugin("DiscordSRV") != null) + DiscordSRV.api.subscribe(DiscordSRVEventListener.instance()); + } + + public static void unsubscribe() { + if (pm.getPlugin("DiscordSRV") != null) + DiscordSRV.api.unsubscribe(DiscordSRVEventListener.instance()); + } +} diff --git a/src/main/java/io/github/axolotsh/hieroglyph/entities/Badge.java b/src/main/java/io/github/axolotsh/hieroglyph/entities/Badge.java new file mode 100644 index 0000000..dca95cb --- /dev/null +++ b/src/main/java/io/github/axolotsh/hieroglyph/entities/Badge.java @@ -0,0 +1,19 @@ +package io.github.axolotsh.hieroglyph.entities; + +import lombok.Builder; +import lombok.Getter; +import lombok.experimental.Accessors; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.key.Keyed; + +@Getter +@Builder +@Accessors(chain = true, fluent = true) +public class Badge implements Keyed { + private transient Key key; + + private String name; + private String description; + + private String icon; +} diff --git a/src/main/java/io/github/axolotsh/hieroglyph/entities/PlayerProfile.java b/src/main/java/io/github/axolotsh/hieroglyph/entities/PlayerProfile.java new file mode 100644 index 0000000..b54988c --- /dev/null +++ b/src/main/java/io/github/axolotsh/hieroglyph/entities/PlayerProfile.java @@ -0,0 +1,164 @@ +package io.github.axolotsh.hieroglyph.entities; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.Nullable; + +import io.github.axolotsh.hieroglyph.Hieroglyph; +import io.github.axolotsh.hieroglyph.HieroglyphConfig; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.key.Keyed; + +@Accessors(chain = true, fluent = true) +public class PlayerProfile implements Keyed { + @Getter + @Nullable + private UUID uuid; + + @Getter + private transient Key key; + + @Setter + @Nullable + private String icon; + + public String icon() { + if (icon == null) + return HieroglyphConfig.PlayerProfile.icon(); + return icon; + } + + @Setter + @Nullable + private String name; + + public String name() { + if (name == null) + return HieroglyphConfig.PlayerProfile.name(); + return name; + } + + @Setter + @Nullable + private String layout; + + public String layout() { + if (layout == null) + return HieroglyphConfig.PlayerProfile.layout(); + return layout; + } + + private final Set badges = new HashSet<>(); + private final Set selectedBadges = new HashSet<>(); + + public PlayerProfile addBadge(Badge badge) { + Hieroglyph.BADGE_REGISTRY.add(badge); + return addBadge(badge.key()); + } + + public PlayerProfile addBadge(Key badge) { + badges.add(badge); + return this; + } + + public PlayerProfile removeBadge(Badge badge) { + return removeBadge(badge.key()); + } + + public PlayerProfile removeBadge(Key badge) { + badges.remove(badge); + return this; + } + + public PlayerProfile clearBadges() { + badges.clear(); + return this; + } + + public Collection getBadges() { + return Hieroglyph.BADGE_REGISTRY.getAll(badges); + } + + public PlayerProfile selectBadge(Badge badge) { + return selectBadge(badge.key()); + } + + public PlayerProfile selectBadge(Key badge) { + if (badges.contains(badge)) + selectedBadges.add(badge); + return this; + } + + public PlayerProfile deselectBadge(Badge badge) { + return deselectBadge(badge.key()); + } + + public PlayerProfile deselectBadge(Key badge) { + selectedBadges.remove(badge); + return this; + } + + public PlayerProfile clearSelectedBadges() { + selectedBadges.clear(); + return this; + } + + public Collection getSelectedBadges() { + return Hieroglyph.BADGE_REGISTRY.getAll(selectedBadges); + } + + private transient Player player; + + @Nullable + public Player player() { + if (uuid == null) + return null; + if (player == null) + player = Bukkit.getPlayer(uuid); + return player; + } + + public String playerName() { + if (uuid == null) + return "UNKNOWN"; + return player().getName(); + } + + public String playerUUID() { + if (uuid == null) + return "UNKNOWN"; + return uuid.toString(); + } + + private PlayerProfile() { + } + + private PlayerProfile(UUID uuid) { + this.uuid = uuid; + this.key = Key.key(uuid.toString()); + } + + public static PlayerProfile fromPlayer(OfflinePlayer player) { + return new PlayerProfile(player.getUniqueId()); + } + + public static PlayerProfile fromPlayer(Player player) { + return new PlayerProfile(player.getUniqueId()); + } + + public static PlayerProfile consoleProfile() { + return new PlayerProfile() + .layout(HieroglyphConfig.ServerProfile.layout()) + .name(HieroglyphConfig.ServerProfile.name()) + .icon(HieroglyphConfig.ServerProfile.icon()); + } +} diff --git a/src/main/java/io/github/axolotsh/hieroglyph/minecraft/MinecraftEventListener.java b/src/main/java/io/github/axolotsh/hieroglyph/minecraft/MinecraftEventListener.java new file mode 100644 index 0000000..30e4616 --- /dev/null +++ b/src/main/java/io/github/axolotsh/hieroglyph/minecraft/MinecraftEventListener.java @@ -0,0 +1,54 @@ +package io.github.axolotsh.hieroglyph.minecraft; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; + +import io.github.axolotsh.hieroglyph.HieroglyphConfig; +import io.github.axolotsh.hieroglyph.HieroglyphFormatter; +import io.github.axolotsh.hieroglyph.commands.GlobalCommand; +import io.github.axolotsh.hieroglyph.commands.LocalCommand; +import io.github.axolotsh.hieroglyph.utils.FormatterContext; +import io.papermc.paper.event.player.AsyncChatEvent; +import lombok.Getter; +import lombok.experimental.Accessors; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; + +@Accessors(fluent = true) +public class MinecraftEventListener implements Listener { + @Getter + private static final MinecraftEventListener instance = new MinecraftEventListener(); + + private MinecraftEventListener() { + } + + @EventHandler + private void onPlayerJoin(PlayerJoinEvent event) { + var player = event.getPlayer(); + + var formatter = HieroglyphFormatter.profile(player); + var component = formatter.formatComponent(FormatterContext.PLAYER_LIST); + player.displayName(component); + player.playerListName(component); + } + + @EventHandler(priority = EventPriority.MONITOR) + private void onPlayerSendChatMessage(AsyncChatEvent event) { + if (event.isCancelled()) + return; + + event.viewers().clear(); + var message = PlainTextComponentSerializer.plainText() + .serialize(event.message()); + + var player = event.getPlayer(); + + if (message.startsWith(HieroglyphConfig.GlobalMessageConfig.prefix())) { + GlobalCommand.globalMessage(player, message); + return; + } + + LocalCommand.localMessage(player, message); + } +} diff --git a/src/main/java/io/github/axolotsh/hieroglyph/minecraft/Plugin.java b/src/main/java/io/github/axolotsh/hieroglyph/minecraft/Plugin.java index 6587a6b..e932f04 100644 --- a/src/main/java/io/github/axolotsh/hieroglyph/minecraft/Plugin.java +++ b/src/main/java/io/github/axolotsh/hieroglyph/minecraft/Plugin.java @@ -1,16 +1,118 @@ package io.github.axolotsh.hieroglyph.minecraft; -import org.bukkit.plugin.java.JavaPlugin; +import java.io.File; +import java.io.IOException; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.plugin.java.JavaPlugin; +import org.slf4j.Logger; + +import io.github.axolotsh.hieroglyph.Hieroglyph; +import io.github.axolotsh.hieroglyph.commands.GlobalCommand; +import io.github.axolotsh.hieroglyph.commands.HieroglyphCommand; +import io.github.axolotsh.hieroglyph.commands.ICommand; +import io.github.axolotsh.hieroglyph.commands.LocalCommand; +import io.github.axolotsh.hieroglyph.commands.PrivateCommand; +import io.github.axolotsh.hieroglyph.discordsrv.DiscordSRVHook; +import io.github.axolotsh.hieroglyph.entities.Badge; +import io.github.axolotsh.hieroglyph.entities.PlayerProfile; +import io.github.axolotsh.localizationlib.LocalizationServiceFactory; +import io.github.axolotsh.localizationlib.LocalizationServiceFactoryBuilder; +import io.github.axolotsh.registrylib.RegistryLoader; +import lombok.Getter; +import lombok.experimental.Accessors; + +@Accessors(fluent = true) public final class Plugin extends JavaPlugin { + @Getter + private static Plugin instance; + @Getter + private static Logger logger; + + @Getter + private static FileConfiguration config; + @Getter + private static LocalizationServiceFactory localizationFactory; + + public Logger getPluginLogger() { + return logger; + } @Override public void onEnable() { - // Plugin startup logic + instance = this; + logger = getSLF4JLogger(); + + saveDefaultConfig(); + reload(); + + registerCommands(); + var pluginManager = getServer().getPluginManager(); + pluginManager.registerEvents(MinecraftEventListener.instance(), this); + + DiscordSRVHook.subscribe(); } @Override public void onDisable() { - // Plugin shutdown logic + saveRegistry(); + DiscordSRVHook.unsubscribe(); + } + + public void reload() { + config = this.getConfig(); + loadLocales(); + loadRegistry(); + } + + private void registerCommands() { + registerCommand(new HieroglyphCommand()); + registerCommand(new GlobalCommand()); + registerCommand(new LocalCommand()); + registerCommand(new PrivateCommand()); + } + + private void registerCommand(ICommand command) { + command.command().register(this); + } + + private void loadLocales() { + var builder = new LocalizationServiceFactoryBuilder(); + var localizationFolder = new File(getDataFolder().getAbsolutePath() + "/locales"); + if (!localizationFolder.exists()) { + saveDefaultLocales(); + } + try { + builder.addDirectory(localizationFolder.getAbsolutePath()); + } catch (IOException e) { + logger.error("Error while loading locales: {}", e.getMessage()); + throw new RuntimeException(e); + } + localizationFactory = builder.build(); + } + + private void saveDefaultLocales() { + saveResource("locales/en_US.json", false); + saveResource("locales/ru_RU.json", false); + } + + private RegistryLoader registryLoader = RegistryLoader.builder() + .logger(getPluginLogger()) + .rootPath(getDataPath().resolve("data")) + .build(); + private static final String BADGE_TYPE = "badge"; + private static final String PLAYER_PROFILE_TYPE = "player_profile"; + + private void loadRegistry() { + Hieroglyph.BADGE_REGISTRY.removeAll(); + Hieroglyph.BADGE_REGISTRY.addAll(registryLoader.loadAll(BADGE_TYPE, Badge.class)); + + Hieroglyph.PLAYER_PROFILE_REGISTRY.removeAll(); + Hieroglyph.PLAYER_PROFILE_REGISTRY.addAll(registryLoader.loadAll(PLAYER_PROFILE_TYPE, PlayerProfile.class)); + } + + private void saveRegistry() { + registryLoader.saveAll(Hieroglyph.BADGE_REGISTRY.getAll(), BADGE_TYPE); + registryLoader.saveAll(Hieroglyph.PLAYER_PROFILE_REGISTRY.getAll(), PLAYER_PROFILE_TYPE); } } diff --git a/src/main/java/io/github/axolotsh/hieroglyph/utils/Formatter.java b/src/main/java/io/github/axolotsh/hieroglyph/utils/Formatter.java new file mode 100644 index 0000000..156b3e2 --- /dev/null +++ b/src/main/java/io/github/axolotsh/hieroglyph/utils/Formatter.java @@ -0,0 +1,123 @@ +package io.github.axolotsh.hieroglyph.utils; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +import edu.umd.cs.findbugs.annotations.Nullable; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; + +// I find net.kyori.adventure.text.minimessage.tag.resolver.TagResolver very useful and convenient, +// but not in this context due to the internal limitations of this library. +// For example, ignoring tags inside the arguments of another tag, +// or formatting in an intermediate string in the place of a component. +@Accessors(chain = true, fluent = true) +public class Formatter { + @Getter + @Setter + @Nullable + private String content; + private Map tags = new LinkedHashMap<>(); + + public Formatter() { + } + + public Formatter(String content) { + this.content = content; + } + + public Set getTags() { + return Set.copyOf(tags.values()); + } + + public Formatter addTag(String tag) { + return addTag(new FormatterTag(tag)); + } + + public Formatter addTags(String... tags) { + for (var tag : tags) + addTag(new FormatterTag(tag)); + return this; + } + + public Formatter addTag(String tag, String replacement) { + return addTag(new FormatterTag(tag, replacement)); + } + + public Formatter addTag(String tag, Function replacement) { + return addTag(new FormatterTag(tag, replacement)); + } + + public Formatter addTags(FormatterTag... tags) { + for (var tag : tags) + addTag(tag); + return this; + } + + public Formatter addTag(FormatterTag tag) { + tags.put(tag.tag(), tag); + return this; + } + + public Formatter addTag(String tag, Formatter hieroglyphFormatter) { + return addTag(tag, ctx -> hieroglyphFormatter.formatString(ctx)); + } + + public Formatter addTags(Formatter hieroglyphFormatter) { + return addTags(hieroglyphFormatter.getTags().toArray(FormatterTag[]::new)); + } + + public Formatter removeTag(String tag) { + tags.remove(tag); + return this; + } + + public Formatter removeTag(FormatterTag tag) { + return removeTag(tag.tag()); + } + + private static final MiniMessage mm = MiniMessage.miniMessage(); + + public Component formatComponent() { + return mm.deserialize(formatString()); + } + + public String formatString() { + return formatString(content); + } + + public Component formatComponent(FormatterContext context) { + return mm.deserialize(formatString(context)); + } + + public String formatString(FormatterContext context) { + return formatString(content, context); + } + + public Component formatComponent(String content) { + return mm.deserialize(formatString(content)); + } + + public String formatString(String content) { + return formatString(content, FormatterContext.CHAT); + } + + public Component formatComponent(String content, FormatterContext context) { + return mm.deserialize(formatString(content, context)); + } + + public String formatString(String content, FormatterContext context) { + for (var tag : tags.values()) + content = content.replace(getTag(tag.tag()), tag.replacement().apply(context)); + return content; + } + + private static String getTag(String tag) { + return "<" + tag + ">"; + } +} diff --git a/src/main/java/io/github/axolotsh/hieroglyph/utils/FormatterContext.java b/src/main/java/io/github/axolotsh/hieroglyph/utils/FormatterContext.java new file mode 100644 index 0000000..621b7f4 --- /dev/null +++ b/src/main/java/io/github/axolotsh/hieroglyph/utils/FormatterContext.java @@ -0,0 +1,9 @@ +package io.github.axolotsh.hieroglyph.utils; + +public enum FormatterContext { + CHAT, + PLAYER_LIST, + CONSOLE, + TOOL_TIP, + PLAYER_NAME +} \ No newline at end of file diff --git a/src/main/java/io/github/axolotsh/hieroglyph/utils/FormatterTag.java b/src/main/java/io/github/axolotsh/hieroglyph/utils/FormatterTag.java new file mode 100644 index 0000000..c1047fc --- /dev/null +++ b/src/main/java/io/github/axolotsh/hieroglyph/utils/FormatterTag.java @@ -0,0 +1,36 @@ +package io.github.axolotsh.hieroglyph.utils; + +import java.util.function.Function; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.experimental.Accessors; + +@Getter +@Accessors(chain = true, fluent = true) +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +public class FormatterTag { + @EqualsAndHashCode.Include + private String tag; + private Function replacement; + + public FormatterTag(String tag) { + this(tag, _ -> ""); + } + + public FormatterTag(String tag, String replacement) { + this(tag, _ -> replacement); + } + + public FormatterTag(String tag, Function replacement) { + this.tag = tag; + this.replacement = replacement; + } + + public FormatterTag(String tag, Formatter formatter) { + this.tag = tag; + this.replacement = ctx -> { + return formatter.formatString(ctx); + }; + } +} \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..7c34023 --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,65 @@ +# To edit the mini-message format, see: https://webui.advntr.dev/ + +player-profile: # TODO + # Available tags: + # - Profile name + # - Profile icon + # - Profile selected badges + layout: "'>" + + # Available tags: + # - Player's uuid + icon: "> " + + # Available tags: + # - Player's nick-name + name: "" + +server-profile: + # Available tags: + # - Profile name + # - Profile icon + # - Profile selected badges + # If absent or null, uses player-profile.layout + layout: null + + icon: " " + name: "[SERVER]" + +badge: + # Available tags: + # - Badge's icon + # - Badge's name + # - Badge's description + layout: "\n'>" + + separator: " " + + # Available tags: + # - Separated badges + wrapper: " []" + + +messages: + global: + prefix: "!" + + # Available tags: + # - Player's layout + # - Player's message + layout: " : " + local: + radius: 100 + + # Available tags: + # - Player's layout + # - Player's message + layout: ": " + private: + # Available tags: + # - Player's layout + # - Receiver's layout + # - Player's message + layout: " -> : " + + diff --git a/src/main/resources/locales/en_US.json b/src/main/resources/locales/en_US.json new file mode 100644 index 0000000..0b86558 --- /dev/null +++ b/src/main/resources/locales/en_US.json @@ -0,0 +1,9 @@ +{ + "meta": { + "locale": "en_US" + }, + "entries": { + "command.message.plugin_reloaded": "Plugin reloaded!", + "command.error.message_yourself": "You can't message yourself!" + } +} \ No newline at end of file diff --git a/src/main/resources/locales/ru_RU.json b/src/main/resources/locales/ru_RU.json new file mode 100644 index 0000000..b7d3785 --- /dev/null +++ b/src/main/resources/locales/ru_RU.json @@ -0,0 +1,9 @@ +{ + "meta": { + "locale": "ru_RU" + }, + "entries": { + "command.message.plugin_reloaded": "Плагин перезагружен!", + "command.error.message_yourself": "Вы не можете написать себе-же!" + } +} \ No newline at end of file diff --git a/src/main/resources/paper-plugin.yml b/src/main/resources/paper-plugin.yml index e72e42c..ac9992b 100755 --- a/src/main/resources/paper-plugin.yml +++ b/src/main/resources/paper-plugin.yml @@ -1,5 +1,5 @@ name: Hieroglyph -description: $description +description: ${description} prefix: Hieroglyph version: '${version}' @@ -10,8 +10,16 @@ load: POSTWORLD authors: [ AXOLOTsh ] website: https://github.com/AXOLOTsh +permissions: + hieroglyph.allow-miniMessage: + description: "Allows to use MiniMessage in global, local, and personal messages" + default: op + dependencies: server: CommandAPI: load: BEFORE - required: true \ No newline at end of file + required: true + DiscordSRV: + load: BEFORE + required: false diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 6540999..8937e12 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,7 +1,7 @@ name: Hieroglyph -description: $description +description: ${description} prefix: Hieroglyph -version: '${version}' +version: ${version} main: io.github.axolotsh.hieroglyph.minecraft.Plugin api-version: '26.1.1' @@ -9,3 +9,10 @@ load: POSTWORLD authors: [ AXOLOTsh ] website: https://github.com/AXOLOTsh + +permissions: + hieroglyph.allow-miniMessage: + description: "Allows to use MiniMessage in global, local, and personal messages" + default: op + +softdepend: [DiscordSRV] \ No newline at end of file