Squashed commit of the following:

commit d55ddcc12462004c500312d7283c4c8ded65cadf
Author: AXOLOTsh <96595812+AXOLOTsh@users.noreply.github.com>
Date:   Tue Apr 21 16:24:18 2026 +0300

    Add DiscordSRV integration

commit 9e86ebc6ea400bc3b6e95153ac067b9d0716928e
Author: AXOLOTsh <96595812+AXOLOTsh@users.noreply.github.com>
Date:   Tue Apr 21 15:43:16 2026 +0300

    Add reload command

commit a7fa9f980ca0508ec8a1d7841ee17bb99701811c
Author: AXOLOTsh <96595812+AXOLOTsh@users.noreply.github.com>
Date:   Tue Apr 21 15:41:37 2026 +0300

    Fix private message error message shown on receiver locale bug

commit 11005fd0eef6a6711fa3b33f19f3b690fa784ffd
Author: AXOLOTsh <96595812+AXOLOTsh@users.noreply.github.com>
Date:   Tue Apr 21 15:04:22 2026 +0300

    Add project files
This commit is contained in:
AXOLOTsh
2026-04-21 16:27:35 +03:00
parent f6770ace35
commit 446ae9de20
28 changed files with 1257 additions and 20 deletions

5
.gitignore vendored
View File

@@ -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

View File

@@ -6,6 +6,17 @@
],
"settings": {
"java.compile.nullAnalysis.mode": "automatic",
"java.configuration.updateBuildConfiguration": "interactive"
"java.configuration.updateBuildConfiguration": "interactive",
"cSpell.words": [
"Dcom",
"gradleup",
"GSON",
"jpenilla",
"localizationlib",
"MINIMESSAGE",
"mojang",
"papermc",
"POSTWORLD"
]
}
}

View File

@@ -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)
}
}
}

View File

@@ -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

View File

@@ -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<PlayerProfile> PLAYER_PROFILE_REGISTRY = HieroglyphRegistry
.initRegistry(PlayerProfile.class);
public static Registry<Badge> BADGE_REGISTRY = HieroglyphRegistry
.initRegistry(Badge.class);
}

View File

@@ -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);
}
}
}

View File

@@ -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<Badge> 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));
}
}

View File

@@ -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<Class<?>, Registry<?>> map = new HashMap<>();
public static <T extends Keyed> void addRegistry(Registry<T> value) {
map.put(value.getClass(), value);
}
@SuppressWarnings("unchecked")
public static <T extends Keyed> Registry<T> getRegistry(Class<T> typeClass) {
return (Registry<T>) map.get(typeClass);
}
@SuppressWarnings("unchecked")
static <T extends Keyed> Registry<T> initRegistry(Class<T> typeClass) {
var registry = (Registry<T>) map.get(typeClass);
if (registry == null) {
registry = new Registry<>(typeClass);
addRegistry(registry);
}
return registry;
}
}

View File

@@ -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);
});
}
}

View File

@@ -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());
}
}

View File

@@ -0,0 +1,7 @@
package io.github.axolotsh.hieroglyph.commands;
import dev.jorel.commandapi.CommandAPICommand;
public interface ICommand {
public CommandAPICommand command();
}

View File

@@ -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<Player>();
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);
});
}
}

View File

@@ -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);
});
}
}

View File

@@ -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);
});
}
}

View File

@@ -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));
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}

View File

@@ -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<Key> badges = new HashSet<>();
private final Set<Key> 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<Badge> 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<Badge> 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());
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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<String, FormatterTag> tags = new LinkedHashMap<>();
public Formatter() {
}
public Formatter(String content) {
this.content = content;
}
public Set<FormatterTag> 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<FormatterContext, String> 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 + ">";
}
}

View File

@@ -0,0 +1,9 @@
package io.github.axolotsh.hieroglyph.utils;
public enum FormatterContext {
CHAT,
PLAYER_LIST,
CONSOLE,
TOOL_TIP,
PLAYER_NAME
}

View File

@@ -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<FormatterContext, String> replacement;
public FormatterTag(String tag) {
this(tag, _ -> "");
}
public FormatterTag(String tag, String replacement) {
this(tag, _ -> replacement);
}
public FormatterTag(String tag, Function<FormatterContext, String> replacement) {
this.tag = tag;
this.replacement = replacement;
}
public FormatterTag(String tag, Formatter formatter) {
this.tag = tag;
this.replacement = ctx -> {
return formatter.formatString(ctx);
};
}
}

View File

@@ -0,0 +1,65 @@
# To edit the mini-message format, see: https://webui.advntr.dev/
player-profile: # TODO
# Available tags:
# <name> - Profile name
# <icon> - Profile icon
# <badges> - Profile selected badges
layout: "<click:suggest_command:'/m <name>'><white><icon></white><name><badges></click>"
# Available tags:
# <uuid> - Player's uuid
icon: "<head:<uuid>> "
# Available tags:
# <name> - Player's nick-name
name: "<name>"
server-profile:
# Available tags:
# <name> - Profile name
# <icon> - Profile icon
# <badges> - Profile selected badges
# If absent or null, uses player-profile.layout
layout: null
icon: "<sprite:blocks:block/command_block_back> "
name: "<red>[SERVER]</red>"
badge:
# Available tags:
# <icon> - Badge's icon
# <name> - Badge's name
# <description> - Badge's description
layout: "<hover:show_text:'<aqua><name></aqua>\n<gray><description></gray>'><icon></hover>"
separator: " "
# Available tags:
# <badges> - Separated badges
wrapper: " [<badges>]"
messages:
global:
prefix: "!"
# Available tags:
# <sender> - Player's layout
# <message> - Player's message
layout: "<dark_green>Ⓖ</dark_green> <yellow><sender>:</yellow> <white><message> </white>"
local:
radius: 100
# Available tags:
# <sender> - Player's layout
# <message> - Player's message
layout: "<gray><sender>:</gray> <white><message> </white>"
private:
# Available tags:
# <sender> - Player's layout
# <receiver> - Receiver's layout
# <message> - Player's message
layout: "<blue><sender> -> <receiver>:</blue> <gray><message> </gray>"

View File

@@ -0,0 +1,9 @@
{
"meta": {
"locale": "en_US"
},
"entries": {
"command.message.plugin_reloaded": "Plugin reloaded!",
"command.error.message_yourself": "<red>You can't message yourself!</red>"
}
}

View File

@@ -0,0 +1,9 @@
{
"meta": {
"locale": "ru_RU"
},
"entries": {
"command.message.plugin_reloaded": "Плагин перезагружен!",
"command.error.message_yourself": "<red>Вы не можете написать себе-же!</red>"
}
}

View File

@@ -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
DiscordSRV:
load: BEFORE
required: false

View File

@@ -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]