+ * Initialized by the platform entry point (e.g. {@code SimpleNicks} on Paper,
+ * {@code SimpleNicksFabric} on Fabric) before any core class is used.
+ * All core classes access the platform through {@link #get()}.
+ *
+ */
+public final class SimpleNicksCore {
+
+ private static SimpleNicksCore instance;
+
+ private final PlatformAdapter platform;
+ private final MiniMessage miniMessage;
+
+ private SimpleNicksCore(@NotNull PlatformAdapter platform) {
+ this.platform = platform;
+ this.miniMessage = platform.getMiniMessage();
+ }
+
+ /**
+ * Initializes the core with the given platform adapter.
+ * Must be called before any core class accesses {@link #get()}.
+ *
+ * @param platform the platform-specific adapter
+ */
+ public static void initialize(@NotNull PlatformAdapter platform) {
+ instance = new SimpleNicksCore(platform);
+ }
+
+ /**
+ * Returns the active core instance.
+ *
+ * @return the core singleton
+ * @throws IllegalStateException if {@link #initialize(PlatformAdapter)} has not been called
+ */
+ @NotNull
+ public static SimpleNicksCore get() {
+ if (instance == null) throw new IllegalStateException("SimpleNicksCore has not been initialized");
+ return instance;
+ }
+
+ /**
+ * Tears down the core instance. Called during plugin/mod shutdown.
+ */
+ public static void shutdown() {
+ instance = null;
+ }
+
+ /**
+ * Returns the platform adapter for this environment.
+ *
+ * @return the platform adapter
+ */
+ @NotNull
+ public PlatformAdapter platform() {
+ return platform;
+ }
+
+ /**
+ * Returns the configured {@link MiniMessage} instance.
+ *
+ * @return the MiniMessage instance
+ */
+ @NotNull
+ public MiniMessage miniMessage() {
+ return miniMessage;
+ }
+}
diff --git a/core/src/main/java/simplexity/simplenicks/commands/NicknameProcessor.java b/core/src/main/java/simplexity/simplenicks/commands/NicknameProcessor.java
new file mode 100644
index 0000000..918776c
--- /dev/null
+++ b/core/src/main/java/simplexity/simplenicks/commands/NicknameProcessor.java
@@ -0,0 +1,147 @@
+package simplexity.simplenicks.commands;
+
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.saving.Cache;
+import simplexity.simplenicks.saving.Nickname;
+import simplexity.simplenicks.saving.SqlHandler;
+
+import org.jetbrains.annotations.Nullable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Handles the high-level logic for nickname management.
+ *
+ * This class acts as the main entry point for commands or other
+ * external systems that want to interact with nicknames.
+ * It delegates persistence and caching to {@link Cache} and {@link SqlHandler}.
+ *
+ */
+@SuppressWarnings("UnusedReturnValue")
+public class NicknameProcessor {
+ private static NicknameProcessor instance;
+
+ private NicknameProcessor() {
+ }
+
+ public static NicknameProcessor getInstance() {
+ if (instance == null) instance = new NicknameProcessor();
+ return instance;
+ }
+
+ /**
+ * Sets a player's active nickname.
+ *
+ * @param uuid the player's UUID
+ * @param username the player's last known username
+ * @param nickname the nickname string to assign
+ * @return {@code true} if the nickname was set successfully,
+ * {@code false} if it failed to persist or cache
+ */
+ public boolean setNickname(@NotNull UUID uuid, @NotNull String username, @NotNull String nickname) {
+ return Cache.getInstance().setActiveNickname(uuid, username, nickname);
+ }
+
+ /**
+ * Resets a player's nickname back to their original username.
+ *
+ * @param uuid the player's UUID
+ * @return {@code true} if the nickname was cleared successfully,
+ * {@code false} if the database update failed
+ */
+ public boolean resetNickname(@NotNull UUID uuid) {
+ return Cache.getInstance().clearCurrentNickname(uuid);
+ }
+
+ /**
+ * Saves a nickname to the player's list of saved nicknames.
+ *
+ * @param uuid the player's UUID
+ * @param username the player's last known username
+ * @param nickname the nickname string to save
+ * @return {@code true} if the nickname was saved successfully,
+ * {@code false} if it failed to persist or cache
+ */
+ public boolean saveNickname(@NotNull UUID uuid, @NotNull String username, @NotNull String nickname) {
+ return Cache.getInstance().saveNickname(uuid, username, nickname);
+ }
+
+ /**
+ * Deletes a previously saved nickname for a player.
+ *
+ * @param uuid the player's UUID
+ * @param nickname the nickname string to delete
+ * @return {@code true} if the nickname was deleted successfully,
+ * {@code false} if no such nickname was found or persistence failed
+ */
+ public boolean deleteNickname(@NotNull UUID uuid, @NotNull String nickname) {
+ return Cache.getInstance().deleteSavedNickname(uuid, nickname);
+ }
+
+ /**
+ * Gets all saved nicknames for a player.
+ *
+ * Uses the in-memory cache if the player is online,
+ * otherwise queries SQL directly.
+ *
+ *
+ * @param uuid the player's UUID
+ * @param isOnline whether the player is currently online
+ * @return a non-null list of {@link Nickname}; empty if none exist
+ */
+ @NotNull
+ public List getSavedNicknames(@NotNull UUID uuid, boolean isOnline) {
+ if (isOnline) return Cache.getInstance().getSavedNicknames(uuid);
+ List nicks = SqlHandler.getInstance().getSavedNicknamesForPlayer(uuid);
+ if (nicks == null) return new ArrayList<>();
+ return nicks;
+ }
+
+ /**
+ * Gets the currently active nickname for a player.
+ *
+ * Uses the in-memory cache if the player is online,
+ * otherwise queries SQL directly.
+ *
+ *
+ * @param uuid the player's UUID
+ * @param isOnline whether the player is currently online
+ * @return the current {@link Nickname}, or {@code null} if none is set
+ */
+ @Nullable
+ public Nickname getCurrentNickname(@NotNull UUID uuid, boolean isOnline) {
+ if (isOnline) return Cache.getInstance().getActiveNickname(uuid);
+ return SqlHandler.getInstance().getCurrentNicknameForPlayer(uuid);
+ }
+
+ /**
+ * Gets the number of saved nicknames for a player.
+ *
+ * Uses the in-memory cache if the player is online,
+ * otherwise queries SQL directly.
+ *
+ *
+ * @param uuid the player's UUID
+ * @param isOnline whether the player is currently online
+ * @return the number of saved nicknames
+ */
+ public int getCurrentSavedNickCount(@NotNull UUID uuid, boolean isOnline) {
+ if (isOnline) return Cache.getInstance().getSavedNickCount(uuid);
+ List savedNicks = SqlHandler.getInstance().getSavedNicknamesForPlayer(uuid);
+ if (savedNicks == null || savedNicks.isEmpty()) return 0;
+ return savedNicks.size();
+ }
+
+ /**
+ * Checks if a player has already saved the given nickname.
+ *
+ * @param uuid the player's UUID
+ * @param nickname the nickname string to search for
+ * @return {@code true} if the player already saved this nickname,
+ * {@code false} otherwise
+ */
+ public boolean playerAlreadySavedThis(@NotNull UUID uuid, @NotNull String nickname) {
+ return SqlHandler.getInstance().userAlreadySavedThisName(uuid, nickname);
+ }
+}
diff --git a/core/src/main/java/simplexity/simplenicks/commands/subcommands/Exceptions.java b/core/src/main/java/simplexity/simplenicks/commands/subcommands/Exceptions.java
new file mode 100644
index 0000000..14c933a
--- /dev/null
+++ b/core/src/main/java/simplexity/simplenicks/commands/subcommands/Exceptions.java
@@ -0,0 +1,106 @@
+package simplexity.simplenicks.commands.subcommands;
+
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import com.mojang.brigadier.exceptions.DynamicCommandExceptionType;
+import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.config.ConfigHandler;
+import simplexity.simplenicks.config.LocaleMessage;
+import simplexity.simplenicks.config.MessageUtils;
+
+/**
+ * Factory methods for Brigadier {@link CommandSyntaxException} instances.
+ *
+ * Exception messages are rendered as plain text by stripping MiniMessage tags,
+ * which works correctly on both Paper and Fabric. Methods are used instead of
+ * static fields to avoid class-load timing issues with {@link SimpleNicksCore}.
+ *
+ */
+public class Exceptions {
+
+ private static String strip(String miniMessage) {
+ return SimpleNicksCore.get().miniMessage().stripTags(miniMessage);
+ }
+
+ public static CommandSyntaxException nickIsNull() {
+ return new SimpleCommandExceptionType(
+ () -> strip(LocaleMessage.ERROR_NICK_IS_NULL.getMessage())
+ ).create();
+ }
+
+ public static CommandSyntaxException emptyNickAfterParse() {
+ return new SimpleCommandExceptionType(
+ () -> strip(LocaleMessage.ERROR_INVALID_NICK_EMPTY.getMessage())
+ ).create();
+ }
+
+ public static CommandSyntaxException cannotSave() {
+ return new SimpleCommandExceptionType(
+ () -> strip(LocaleMessage.ERROR_SAVE_FAILURE.getMessage())
+ ).create();
+ }
+
+ public static CommandSyntaxException tooManySavedNames() {
+ return new SimpleCommandExceptionType(
+ () -> strip(LocaleMessage.ERROR_TOO_MANY_TO_SAVE.getMessage())
+ ).create();
+ }
+
+ public static CommandSyntaxException tagsNotPermitted() {
+ return new SimpleCommandExceptionType(
+ () -> strip(LocaleMessage.ERROR_INVALID_TAGS.getMessage())
+ ).create();
+ }
+
+ public static CommandSyntaxException alreadySaved() {
+ return new SimpleCommandExceptionType(
+ () -> strip(LocaleMessage.ERROR_ALREADY_SAVED.getMessage())
+ ).create();
+ }
+
+ public static CommandSyntaxException lengthError(Object nickname) {
+ return new DynamicCommandExceptionType(
+ nick -> () -> strip(SimpleNicksCore.get().miniMessage().stripTags(
+ LocaleMessage.ERROR_INVALID_NICK_LENGTH.getMessage()
+ .replace("", String.valueOf(ConfigHandler.getInstance().getMaxLength()))
+ .replace("", nick.toString())
+ ))
+ ).create(nickname);
+ }
+
+ public static CommandSyntaxException regexError(Object nickname) {
+ return new DynamicCommandExceptionType(
+ nick -> () -> strip(
+ LocaleMessage.ERROR_INVALID_NICK.getMessage()
+ .replace("", ConfigHandler.getInstance().getRegexString())
+ )
+ ).create(nickname);
+ }
+
+ public static CommandSyntaxException invalidPlayerSpecified(Object playerName) {
+ return new DynamicCommandExceptionType(
+ name -> () -> strip(
+ LocaleMessage.ERROR_INVALID_PLAYER.getMessage()
+ .replace("", name.toString())
+ )
+ ).create(playerName);
+ }
+
+ public static CommandSyntaxException nicknameSomeonesUsername(Object nickname) {
+ return new DynamicCommandExceptionType(
+ nick -> () -> strip(
+ LocaleMessage.ERROR_INVALID_OTHER_PLAYERS_USERNAME.getMessage()
+ .replace("", nick.toString())
+ )
+ ).create(nickname);
+ }
+
+ public static CommandSyntaxException someoneUsingThatNickname(Object nickname) {
+ return new DynamicCommandExceptionType(
+ nick -> () -> strip(
+ LocaleMessage.ERROR_INVALID_OTHER_PLAYERS_NICKNAME.getMessage()
+ .replace("", nick.toString())
+ )
+ ).create(nickname);
+ }
+}
diff --git a/src/main/java/simplexity/simplenicks/config/ConfigHandler.java b/core/src/main/java/simplexity/simplenicks/config/ConfigHandler.java
similarity index 86%
rename from src/main/java/simplexity/simplenicks/config/ConfigHandler.java
rename to core/src/main/java/simplexity/simplenicks/config/ConfigHandler.java
index 2111b3e..cd2927b 100644
--- a/src/main/java/simplexity/simplenicks/config/ConfigHandler.java
+++ b/core/src/main/java/simplexity/simplenicks/config/ConfigHandler.java
@@ -1,31 +1,21 @@
package simplexity.simplenicks.config;
-import org.bukkit.configuration.file.FileConfiguration;
-import simplexity.simplenicks.SimpleNicks;
+import org.slf4j.Logger;
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.platform.ConfigProvider;
-import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
public class ConfigHandler {
- public String getRegexString() {
- return regexString;
- }
-
- public String getNickPrefix() {
- return nickPrefix;
- }
-
-
private static ConfigHandler instance;
- private final Logger logger = SimpleNicks.getSimpleNicksLogger();
private Pattern regex;
- private boolean mySql, tablistNick, usernameProtection, onlineNickProtection, offlineNickProtection, debugMode, nickRequiresPermission,
- colorRequiresPermission, formatRequiresPermission, whoRequiresPermission;
+ private boolean mySql, tablistNick, usernameProtection, onlineNickProtection, offlineNickProtection, debugMode,
+ nickRequiresPermission, colorRequiresPermission, formatRequiresPermission, whoRequiresPermission;
private int maxLength, maxSaves;
- private final int MILLI_PER_DAY = 86_400_000;
+ private static final int MILLI_PER_DAY = 86_400_000;
private String regexString, nickPrefix, mySqlIp, mySqlName, mySqlUsername, mySqlPassword;
private long usernameProtectionTime, offlineNickProtectionTime = 0;
@@ -38,17 +28,20 @@ public static ConfigHandler getInstance() {
return instance;
}
+ private static Logger logger() {
+ return SimpleNicksCore.get().platform().getLogger();
+ }
+
public void reloadConfig() {
- SimpleNicks.getInstance().reloadConfig();
+ ConfigProvider config = SimpleNicksCore.get().platform().getConfigProvider();
+ config.reload();
LocaleHandler.getInstance().reloadLocale();
- FileConfiguration config = SimpleNicks.getInstance().getConfig();
- // Check the validity of the regex.
try {
String regexSetting = config.getString("nickname-regex", "[A-Za-z0-9_]+");
regexString = regexSetting;
regex = Pattern.compile(regexSetting);
} catch (PatternSyntaxException e) {
- logger.severe(LocaleMessage.ERROR_INVALID_CONFIG_REGEX.getMessage());
+ logger().error(LocaleMessage.ERROR_INVALID_CONFIG_REGEX.getMessage(), e);
}
debugMode = config.getBoolean("debug-mode", false);
mySql = config.getBoolean("mysql.enabled", false);
@@ -71,11 +64,14 @@ public void reloadConfig() {
offlineNickProtectionTime = config.getLong("nickname-protection.offline.expires", 30) * MILLI_PER_DAY;
}
-
public Pattern getRegex() {
return regex;
}
+ public String getRegexString() {
+ return regexString;
+ }
+
public int getMaxLength() {
return maxLength;
}
@@ -147,4 +143,8 @@ public boolean isWhoRequiresPermission() {
public boolean isUsernameProtection() {
return usernameProtection;
}
+
+ public String getNickPrefix() {
+ return nickPrefix;
+ }
}
diff --git a/core/src/main/java/simplexity/simplenicks/config/LocaleHandler.java b/core/src/main/java/simplexity/simplenicks/config/LocaleHandler.java
new file mode 100644
index 0000000..896fccb
--- /dev/null
+++ b/core/src/main/java/simplexity/simplenicks/config/LocaleHandler.java
@@ -0,0 +1,45 @@
+package simplexity.simplenicks.config;
+
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.platform.ConfigProvider;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+@SuppressWarnings("CallToPrintStackTrace")
+public class LocaleHandler {
+
+ private static LocaleHandler instance;
+
+ private LocaleHandler() {
+ }
+
+ public static LocaleHandler getInstance() {
+ if (instance == null) {
+ instance = new LocaleHandler();
+ }
+ return instance;
+ }
+
+ public void reloadLocale() {
+ ConfigProvider locale = SimpleNicksCore.get().platform().getLocaleProvider();
+ locale.reload();
+ populateLocale(locale);
+ locale.save();
+ }
+
+ private void populateLocale(ConfigProvider locale) {
+ Set missing = new HashSet<>(Arrays.asList(LocaleMessage.values()));
+ for (LocaleMessage localeMessage : LocaleMessage.values()) {
+ if (locale.contains(localeMessage.getPath())) {
+ localeMessage.setMessage(locale.getString(localeMessage.getPath(), localeMessage.getDefaultMessage()));
+ missing.remove(localeMessage);
+ }
+ }
+ for (LocaleMessage localeMessage : missing) {
+ locale.set(localeMessage.getPath(), localeMessage.getDefaultMessage());
+ localeMessage.setMessage(localeMessage.getDefaultMessage());
+ }
+ }
+}
diff --git a/src/main/java/simplexity/simplenicks/config/LocaleMessage.java b/core/src/main/java/simplexity/simplenicks/config/LocaleMessage.java
similarity index 95%
rename from src/main/java/simplexity/simplenicks/config/LocaleMessage.java
rename to core/src/main/java/simplexity/simplenicks/config/LocaleMessage.java
index e469d8b..ca34e99 100644
--- a/src/main/java/simplexity/simplenicks/config/LocaleMessage.java
+++ b/core/src/main/java/simplexity/simplenicks/config/LocaleMessage.java
@@ -15,7 +15,6 @@ public enum LocaleMessage {
HELP_ADMIN_DELETE("plugin.help.admin.delete", "/nick admin delete (username) (nickname) -Delete a saved nickname from a player"),
HELP_ADMIN_LOOKUP("plugin.help.admin.lookup", "/nick admin lookup (username) -Look up a player's nickname info"),
HELP_RELOAD("plugin.help.reload", "/nick reload -Reload the plugin configuration"),
- SHOWN_HELP("plugin.user-shown-help", " has been shown the help screen"),
CONFIG_RELOADED("plugin.config-reloaded", "SimpleNicks config and locale reloaded"),
SERVER_DISPLAY_NAME("plugin.server-display-name", "[Server]"),
@@ -84,10 +83,12 @@ public enum LocaleMessage {
private final String path;
+ private final String defaultMessage;
private String message;
LocaleMessage(String path, String message) {
this.path = path;
+ this.defaultMessage = message;
this.message = message;
}
@@ -102,6 +103,17 @@ public String getMessage() {
return message;
}
+ /**
+ * Returns the hardcoded default message as declared in the enum.
+ * Used when writing missing keys back to a freshly created locale file.
+ *
+ * @return the default message string
+ */
+ @NotNull
+ public String getDefaultMessage() {
+ return defaultMessage;
+ }
+
public void setMessage(@Nullable String message) {
if (message == null) message = "";
this.message = message;
diff --git a/src/main/java/simplexity/simplenicks/config/MessageUtils.java b/core/src/main/java/simplexity/simplenicks/config/MessageUtils.java
similarity index 82%
rename from src/main/java/simplexity/simplenicks/config/MessageUtils.java
rename to core/src/main/java/simplexity/simplenicks/config/MessageUtils.java
index 86e0460..438690d 100644
--- a/src/main/java/simplexity/simplenicks/config/MessageUtils.java
+++ b/core/src/main/java/simplexity/simplenicks/config/MessageUtils.java
@@ -1,21 +1,17 @@
package simplexity.simplenicks.config;
import net.kyori.adventure.text.Component;
-import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
-import simplexity.simplenicks.SimpleNicks;
+import simplexity.simplenicks.SimpleNicksCore;
import simplexity.simplenicks.saving.Nickname;
import java.util.List;
public class MessageUtils {
-
- private static final MiniMessage miniMessage = SimpleNicks.getMiniMessage();
-
@NotNull
public static TagResolver getTimeFormat(long timeSeconds) {
long seconds = timeSeconds % 60;
@@ -24,7 +20,7 @@ public static TagResolver getTimeFormat(long timeSeconds) {
long days = (timeSeconds / (60 * 60 * 24)) % 365;
if (days == 0 && hours == 0 && minutes == 0 && seconds == 0) {
- Component nowComponent = miniMessage.deserialize(LocaleMessage.TIME_FORMAT_NOW.getMessage());
+ Component nowComponent = SimpleNicksCore.get().miniMessage().deserialize(LocaleMessage.TIME_FORMAT_NOW.getMessage());
return TagResolver.resolver(Placeholder.component("time", nowComponent));
}
@@ -59,7 +55,7 @@ public static TagResolver getTimeFormat(long timeSeconds) {
);
}
finalComponent = finalComponent.append(
- miniMessage.deserialize(LocaleMessage.TIME_FORMAT_AGO.getMessage())
+ SimpleNicksCore.get().miniMessage().deserialize(LocaleMessage.TIME_FORMAT_AGO.getMessage())
);
return TagResolver.resolver(Placeholder.component("time", finalComponent));
@@ -69,13 +65,13 @@ public static TagResolver getTimeFormat(long timeSeconds) {
public static TagResolver savedNickListResolver(@Nullable List nicknames) {
if (nicknames == null || nicknames.isEmpty()) {
return TagResolver.resolver(Placeholder.component("list",
- miniMessage.deserialize(LocaleMessage.LOOKUP_NO_SAVED_NICKS.getMessage())));
+ SimpleNicksCore.get().miniMessage().deserialize(LocaleMessage.LOOKUP_NO_SAVED_NICKS.getMessage())));
}
Component finalComponent = Component.empty();
for (Nickname nick : nicknames) {
- Component nickname = SimpleNicks.getMiniMessage().deserialize(nick.getNickname());
+ Component nickname = SimpleNicksCore.get().miniMessage().deserialize(nick.getNickname());
finalComponent = finalComponent.append(
- miniMessage.deserialize(
+ SimpleNicksCore.get().miniMessage().deserialize(
LocaleMessage.LOOKUP_SAVED_NICK.getMessage(),
Placeholder.component("name", nickname)
));
@@ -85,9 +81,7 @@ public static TagResolver savedNickListResolver(@Nullable List nicknam
@NotNull
private static Component parseNumber(@NotNull String message, long number) {
- return miniMessage.deserialize(message,
+ return SimpleNicksCore.get().miniMessage().deserialize(message,
Placeholder.parsed("count", String.valueOf(number)));
}
-
-
}
diff --git a/src/main/java/simplexity/simplenicks/logic/NickUtils.java b/core/src/main/java/simplexity/simplenicks/logic/NickUtils.java
similarity index 54%
rename from src/main/java/simplexity/simplenicks/logic/NickUtils.java
rename to core/src/main/java/simplexity/simplenicks/logic/NickUtils.java
index c0513c6..7b797d8 100644
--- a/src/main/java/simplexity/simplenicks/logic/NickUtils.java
+++ b/core/src/main/java/simplexity/simplenicks/logic/NickUtils.java
@@ -4,14 +4,12 @@
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
-import org.bukkit.Bukkit;
-import org.bukkit.OfflinePlayer;
-import org.bukkit.command.CommandSender;
-import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
-import simplexity.simplenicks.SimpleNicks;
+import simplexity.simplenicks.SimpleNicksCore;
import simplexity.simplenicks.commands.subcommands.Exceptions;
import simplexity.simplenicks.config.ConfigHandler;
+import simplexity.simplenicks.platform.PlayerInfo;
+import simplexity.simplenicks.platform.SenderContext;
import simplexity.simplenicks.saving.Cache;
import simplexity.simplenicks.saving.Nickname;
import simplexity.simplenicks.saving.SqlHandler;
@@ -19,6 +17,8 @@
import simplexity.simplenicks.util.FormatTag;
import simplexity.simplenicks.util.NickPermission;
+import org.jetbrains.annotations.Nullable;
+
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@@ -35,47 +35,46 @@
@SuppressWarnings("UnusedReturnValue")
public class NickUtils {
- private static final MiniMessage miniMessage = SimpleNicks.getMiniMessage();
-
+ private static MiniMessage mm() {
+ return SimpleNicksCore.get().miniMessage();
+ }
/**
* Performs all configured checks on a nickname, including length, regex,
* username conflicts, and nickname protection. Throws a {@link CommandSyntaxException}
* if any of the checks fail.
*
- * @param sender The sender attempting to set the nickname
- * @param nickname The nickname to validate
+ * @param sender the sender attempting to set the nickname
+ * @param nickname the nickname to validate
* @throws CommandSyntaxException if any of the nickname checks fail
*/
- public static void nicknameChecks(@NotNull CommandSender sender, @NotNull Nickname nickname) throws CommandSyntaxException {
+ public static void nicknameChecks(@NotNull SenderContext sender, @NotNull Nickname nickname) throws CommandSyntaxException {
String normalizedNick = nickname.getNormalizedNickname();
if (normalizedNick.isEmpty()) {
- throw Exceptions.ERROR_EMPTY_NICK_AFTER_PARSE.create();
+ throw Exceptions.emptyNickAfterParse();
}
- boolean bypassUsername = sender.hasPermission(NickPermission.NICK_BYPASS_USERNAME.getPermission());
- boolean bypassLength = sender.hasPermission(NickPermission.NICK_BYPASS_LENGTH.getPermission());
- boolean bypassRegex = sender.hasPermission(NickPermission.NICK_BYPASS_REGEX.getPermission());
- boolean bypassNickProtection = sender.hasPermission(NickPermission.NICK_BYPASS_NICK_PROTECTION.getPermission());
+ boolean bypassUsername = sender.hasPermission(NickPermission.NICK_BYPASS_USERNAME.getPermissionKey());
+ boolean bypassLength = sender.hasPermission(NickPermission.NICK_BYPASS_LENGTH.getPermissionKey());
+ boolean bypassRegex = sender.hasPermission(NickPermission.NICK_BYPASS_REGEX.getPermissionKey());
+ boolean bypassNickProtection = sender.hasPermission(NickPermission.NICK_BYPASS_NICK_PROTECTION.getPermissionKey());
- if (!bypassUsername) {
- if (ConfigHandler.getInstance().isUsernameProtection() && isProtectedUsername(normalizedNick))
- throw Exceptions.ERROR_NICKNAME_IS_SOMEONES_USERNAME.create(normalizedNick);
+ if (!bypassUsername && ConfigHandler.getInstance().isUsernameProtection() && isProtectedUsername(normalizedNick)) {
+ throw Exceptions.nicknameSomeonesUsername(normalizedNick);
}
- if (!bypassLength) {
- if (normalizedNick.length() > ConfigHandler.getInstance().getMaxLength())
- throw Exceptions.ERROR_LENGTH.create(normalizedNick);
+ if (!bypassLength && normalizedNick.length() > ConfigHandler.getInstance().getMaxLength()) {
+ throw Exceptions.lengthError(normalizedNick);
}
- if (!bypassRegex) {
- if (!passesRegexCheck(normalizedNick)) throw Exceptions.ERROR_REGEX.create(normalizedNick);
+ if (!bypassRegex && !passesRegexCheck(normalizedNick)) {
+ throw Exceptions.regexError(normalizedNick);
}
- if (ConfigHandler.getInstance().shouldOnlineNicksBeProtected() && !bypassNickProtection) {
- if (someoneOnlineUsingThis(sender, normalizedNick))
- throw Exceptions.ERROR_SOMEONE_USING_THAT_NICKNAME.create(normalizedNick);
- }
- if (ConfigHandler.getInstance().shouldOfflineNicksBeProtected() && !bypassNickProtection) {
- if (someoneSavedUsingThis(sender, normalizedNick))
- throw Exceptions.ERROR_SOMEONE_USING_THAT_NICKNAME.create(normalizedNick);
+ if (!bypassNickProtection) {
+ if (ConfigHandler.getInstance().shouldOnlineNicksBeProtected() && someoneOnlineUsingThis(sender, normalizedNick)) {
+ throw Exceptions.someoneUsingThatNickname(normalizedNick);
+ }
+ if (ConfigHandler.getInstance().shouldOfflineNicksBeProtected() && someoneSavedUsingThis(sender, normalizedNick)) {
+ throw Exceptions.someoneUsingThatNickname(normalizedNick);
+ }
}
}
@@ -83,57 +82,57 @@ public static void nicknameChecks(@NotNull CommandSender sender, @NotNull Nickna
* Updates a player's display name and optionally their tab list name to reflect their
* active nickname.
*
- * @param uuid The UUID of the player whose display name should be refreshed
+ * @param uuid the UUID of the player whose display name should be refreshed
* @return true if the player's display name was successfully refreshed, false if the player is offline
*/
public static boolean refreshDisplayName(@NotNull UUID uuid) {
- Player player = Bukkit.getPlayer(uuid);
- if (player == null) return false;
+ if (!SimpleNicksCore.get().platform().isPlayerOnline(uuid)) return false;
Nickname nickname = Cache.getInstance().getActiveNickname(uuid);
if (nickname == null) {
- player.displayName(null);
+ SimpleNicksCore.get().platform().clearDisplayName(uuid);
+ if (ConfigHandler.getInstance().shouldNickTablist()) {
+ SimpleNicksCore.get().platform().clearTablistName(uuid);
+ }
return true;
}
- Component displayName = miniMessage.deserialize(ConfigHandler.getInstance().getNickPrefix())
- .append(SimpleNicks.getMiniMessage().deserialize(nickname.getNickname()));
- player.displayName(displayName);
+ Component displayName = mm().deserialize(ConfigHandler.getInstance().getNickPrefix())
+ .append(mm().deserialize(nickname.getNickname()));
+ SimpleNicksCore.get().platform().setDisplayName(uuid, displayName);
if (ConfigHandler.getInstance().shouldNickTablist()) {
- player.playerListName(SimpleNicks.getMiniMessage().deserialize(nickname.getNickname()));
+ SimpleNicksCore.get().platform().setTablistName(uuid, mm().deserialize(nickname.getNickname()));
}
return true;
}
-
/**
- * Checks whether the given nickname only uses tags and formatting that the player
+ * Checks whether the given nickname only uses tags and formatting that the sender
* has permission to use.
*
- * @param user The command sender attempting to use the nickname
- * @param nick The nickname to validate
+ * @param user the sender attempting to use the nickname
+ * @param nick the nickname to validate
* @return true if the nickname only uses allowed tags, false otherwise
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
- public static boolean isValidTags(@NotNull CommandSender user, @NotNull String nick) {
+ public static boolean isValidTags(@NotNull SenderContext user, @NotNull String nick) {
TagResolver.Builder resolver = TagResolver.builder();
for (ColorTag colorTag : ColorTag.values()) {
- if (user.hasPermission(colorTag.getPermission()) || !ConfigHandler.getInstance().isColorRequiresPermission()) {
+ if (user.hasPermission(colorTag.getPermissionKey()) || !ConfigHandler.getInstance().isColorRequiresPermission()) {
resolver.resolver(colorTag.getTagResolver());
}
}
for (FormatTag formatTag : FormatTag.values()) {
- if (user.hasPermission(formatTag.getPermission()) || !ConfigHandler.getInstance().isFormatRequiresPermission()) {
+ if (user.hasPermission(formatTag.getPermissionKey()) || !ConfigHandler.getInstance().isFormatRequiresPermission()) {
resolver.resolver(formatTag.getTagResolver());
}
}
-
MiniMessage parser = MiniMessage.builder().strict(false).tags(resolver.build()).build();
- Component defaultParsed = miniMessage.deserialize(nick);
- String defaultSerialized = miniMessage.serialize(defaultParsed);
+ Component defaultParsed = mm().deserialize(nick);
+ String defaultSerialized = mm().serialize(defaultParsed);
Component permissionParsed = parser.deserialize(nick);
- String permissionSerialized = miniMessage.serialize(permissionParsed);
+ String permissionSerialized = mm().serialize(permissionParsed);
return defaultSerialized.equals(permissionSerialized);
}
@@ -142,37 +141,42 @@ public static boolean isValidTags(@NotNull CommandSender user, @NotNull String n
* Converts a nickname into a "normalized" version by stripping all MiniMessage tags
* and converting to lowercase. Used for comparisons and storage.
*
- * @param nickname The nickname to normalize
- * @return The normalized nickname string
+ * @param nickname the nickname to normalize
+ * @return the normalized nickname string
*/
public static String normalizeNickname(@NotNull String nickname) {
- return miniMessage.stripTags(nickname).toLowerCase();
+ return mm().stripTags(nickname).toLowerCase();
}
/**
- * Retrieves offline players who have saved a specific normalized nickname.
+ * Retrieves players who have a specific normalized nickname set as their active nickname.
+ *
+ * Uses {@link SqlHandler#playerSaveExists(UUID)} instead of platform APIs to determine
+ * whether a UUID belongs to a known player.
+ *
*
- * @param normalizedNickname The normalized nickname to search for
- * @return A list of OfflinePlayer objects who have used the nickname,
- * or null if no players were found
+ * @param normalizedNickname the normalized nickname to search for
+ * @return a list of {@link PlayerInfo} for players who use the nickname
*/
@NotNull
- public static List getOfflinePlayersByNickname(@NotNull String normalizedNickname) {
+ public static List getPlayersByNickname(@NotNull String normalizedNickname) {
List usersWithThisName = SqlHandler.getInstance().getUuidsOfNickname(normalizedNickname);
if (usersWithThisName == null || usersWithThisName.isEmpty()) return new ArrayList<>();
- List playersByNick = new ArrayList<>();
+ List result = new ArrayList<>();
for (UUID uuid : usersWithThisName) {
- OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(uuid);
- if (!offlinePlayer.hasPlayedBefore()) continue;
- playersByNick.add(offlinePlayer);
+ if (!SqlHandler.getInstance().playerSaveExists(uuid)) continue;
+ String username = SimpleNicksCore.get().platform().getPlayerUsername(uuid)
+ .orElseGet(() -> uuid.toString());
+ long lastLogin = SqlHandler.getInstance().getLastLoginMillis(uuid);
+ result.add(new PlayerInfo(uuid, username, lastLogin));
}
- return playersByNick;
+ return result;
}
/**
* Checks if a normalized nickname passes the regex pattern defined in the configuration.
*
- * @param normalizedNick The normalized nickname to validate
+ * @param normalizedNick the normalized nickname to validate
* @return true if the nickname matches the regex, false otherwise
*/
public static boolean passesRegexCheck(@NotNull String normalizedNick) {
@@ -185,12 +189,14 @@ public static boolean passesRegexCheck(@NotNull String normalizedNick) {
* This prevents nicknames from being set to usernames of other players within the
* protection period.
*
- * @param normalizedName The normalized nickname to check
+ * @param normalizedName the normalized nickname to check
* @return true if the nickname matches a protected username, false otherwise
*/
public static boolean isProtectedUsername(@NotNull String normalizedName) {
normalizedName = normalizedName.toLowerCase();
- long expireTime = ConfigHandler.getInstance().getUsernameProtectionTime() == -1 ? System.currentTimeMillis() - ConfigHandler.getInstance().getUsernameProtectionTime() : -1;
+ long expireTime = ConfigHandler.getInstance().getUsernameProtectionTime() == -1
+ ? System.currentTimeMillis() - ConfigHandler.getInstance().getUsernameProtectionTime()
+ : -1;
return SqlHandler.getInstance().lastLoginOfUsername(normalizedName, expireTime) != null;
}
@@ -198,13 +204,12 @@ public static boolean isProtectedUsername(@NotNull String normalizedName) {
* Checks if an online player (other than the sender) is currently using the given
* normalized nickname.
*
- * @param sender The command sender attempting to set the nickname
- * @param normalizedNick The normalized nickname to check
+ * @param sender the sender attempting to set the nickname
+ * @param normalizedNick the normalized nickname to check
* @return true if another online player is using this nickname, false otherwise
*/
- public static boolean someoneOnlineUsingThis(@NotNull CommandSender sender, @NotNull String normalizedNick) {
- UUID playerUuid = null;
- if (sender instanceof Player playerSender) playerUuid = playerSender.getUniqueId();
+ public static boolean someoneOnlineUsingThis(@NotNull SenderContext sender, @NotNull String normalizedNick) {
+ UUID playerUuid = sender.getUuid().orElse(null);
return Cache.getInstance().nickInUseOnlinePlayers(playerUuid, normalizedNick);
}
@@ -212,21 +217,19 @@ public static boolean someoneOnlineUsingThis(@NotNull CommandSender sender, @Not
* Checks if the given normalized nickname is already saved by another player and is
* protected based on offline nickname protection settings.
*
- * @param sender The command sender attempting to set the nickname
- * @param normalizedNick The normalized nickname to check
+ * @param sender the sender attempting to set the nickname
+ * @param normalizedNick the normalized nickname to check
* @return true if the nickname is already saved and protected, false otherwise
*/
- public static boolean someoneSavedUsingThis(@NotNull CommandSender sender, @NotNull String normalizedNick) {
- UUID senderUuid = null;
- if (sender instanceof Player playerSender) senderUuid = playerSender.getUniqueId();
+ public static boolean someoneSavedUsingThis(@NotNull SenderContext sender, @NotNull String normalizedNick) {
+ UUID senderUuid = sender.getUuid().orElse(null);
List uuidsWithThis = SqlHandler.getInstance().nickAlreadySavedTo(senderUuid, normalizedNick);
if (uuidsWithThis == null || uuidsWithThis.isEmpty()) return false;
for (UUID uuid : uuidsWithThis) {
- if (SqlHandler.getInstance().lastLoginOfUuid(uuid, ConfigHandler.getInstance().getOfflineNickProtectionTime()) != null)
+ if (SqlHandler.getInstance().lastLoginOfUuid(uuid, ConfigHandler.getInstance().getOfflineNickProtectionTime()) != null) {
return true;
+ }
}
return false;
}
-
-
}
diff --git a/core/src/main/java/simplexity/simplenicks/platform/ConfigProvider.java b/core/src/main/java/simplexity/simplenicks/platform/ConfigProvider.java
new file mode 100644
index 0000000..728b17d
--- /dev/null
+++ b/core/src/main/java/simplexity/simplenicks/platform/ConfigProvider.java
@@ -0,0 +1,74 @@
+package simplexity.simplenicks.platform;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Abstracts YAML file reading and writing so that core config/locale handlers
+ * do not depend on platform-specific config APIs (e.g. Bukkit {@code FileConfiguration}).
+ */
+public interface ConfigProvider {
+
+ /**
+ * Reloads values from the backing file.
+ */
+ void reload();
+
+ /**
+ * Returns the string value at {@code key}, or {@code defaultValue} if absent.
+ *
+ * @param key the config key
+ * @param defaultValue fallback value
+ * @return the string value, or {@code defaultValue}
+ */
+ @Nullable
+ String getString(@NotNull String key, @Nullable String defaultValue);
+
+ /**
+ * Returns the int value at {@code key}, or {@code defaultValue} if absent or not an int.
+ *
+ * @param key the config key
+ * @param defaultValue fallback value
+ * @return the int value, or {@code defaultValue}
+ */
+ int getInt(@NotNull String key, int defaultValue);
+
+ /**
+ * Returns the boolean value at {@code key}, or {@code defaultValue} if absent.
+ *
+ * @param key the config key
+ * @param defaultValue fallback value
+ * @return the boolean value, or {@code defaultValue}
+ */
+ boolean getBoolean(@NotNull String key, boolean defaultValue);
+
+ /**
+ * Returns the long value at {@code key}, or {@code defaultValue} if absent.
+ *
+ * @param key the config key
+ * @param defaultValue fallback value
+ * @return the long value, or {@code defaultValue}
+ */
+ long getLong(@NotNull String key, long defaultValue);
+
+ /**
+ * Sets a value in the backing store. Does not persist until {@link #save()} is called.
+ *
+ * @param key the config key
+ * @param value the value to set
+ */
+ void set(@NotNull String key, @Nullable Object value);
+
+ /**
+ * Persists the current state of the backing store to disk.
+ */
+ void save();
+
+ /**
+ * Returns whether the given key exists in the backing store.
+ *
+ * @param key the config key
+ * @return {@code true} if the key is present
+ */
+ boolean contains(@NotNull String key);
+}
diff --git a/core/src/main/java/simplexity/simplenicks/platform/PlatformAdapter.java b/core/src/main/java/simplexity/simplenicks/platform/PlatformAdapter.java
new file mode 100644
index 0000000..bafb491
--- /dev/null
+++ b/core/src/main/java/simplexity/simplenicks/platform/PlatformAdapter.java
@@ -0,0 +1,140 @@
+package simplexity.simplenicks.platform;
+
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.minimessage.MiniMessage;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Optional;
+import java.util.UUID;
+
+/**
+ * Abstracts all platform-specific operations (scheduling, player access, display names, permissions).
+ *
+ * Each platform (Paper, Fabric) provides its own implementation. Core logic accesses the
+ * platform exclusively through this interface via {@link simplexity.simplenicks.SimpleNicksCore}.
+ *
+ */
+public interface PlatformAdapter {
+
+ /**
+ * Runs a task on a background thread.
+ *
+ * @param task the task to run asynchronously
+ */
+ void runAsync(@NotNull Runnable task);
+
+ /**
+ * Runs a task on the main server thread.
+ *
+ * @param task the task to run synchronously
+ */
+ void runSync(@NotNull Runnable task);
+
+ /**
+ * Returns whether a player is currently online.
+ *
+ * @param uuid the player's UUID
+ * @return {@code true} if the player is online
+ */
+ boolean isPlayerOnline(@NotNull UUID uuid);
+
+ /**
+ * Returns the username of a player if they are currently online.
+ *
+ * @param uuid the player's UUID
+ * @return the username, or empty if offline
+ */
+ @NotNull
+ Optional getPlayerUsername(@NotNull UUID uuid);
+
+ /**
+ * Returns the UUIDs of all currently online players.
+ *
+ * @return collection of online player UUIDs
+ */
+ @NotNull
+ Collection getOnlinePlayers();
+
+ /**
+ * Sets the display name shown in chat and above the player's head.
+ *
+ * @param uuid the player's UUID
+ * @param displayName the Adventure component to display
+ */
+ void setDisplayName(@NotNull UUID uuid, @NotNull Component displayName);
+
+ /**
+ * Sets the name shown in the tab list.
+ *
+ * @param uuid the player's UUID
+ * @param tablistName the Adventure component to display
+ */
+ void setTablistName(@NotNull UUID uuid, @NotNull Component tablistName);
+
+ /**
+ * Clears the player's display name, reverting to their username.
+ *
+ * @param uuid the player's UUID
+ */
+ void clearDisplayName(@NotNull UUID uuid);
+
+ /**
+ * Clears the player's tab list name, reverting to their username.
+ *
+ * @param uuid the player's UUID
+ */
+ void clearTablistName(@NotNull UUID uuid);
+
+ /**
+ * Checks whether the given player has a permission node.
+ *
+ * @param uuid the player's UUID
+ * @param permission the permission node string
+ * @return {@code true} if the player has the permission
+ */
+ boolean hasPermission(@NotNull UUID uuid, @NotNull String permission);
+
+ /**
+ * Returns the plugin's data directory (where config and database files are stored).
+ *
+ * @return the data directory path
+ */
+ @NotNull
+ Path getDataDirectory();
+
+ /**
+ * Returns the plugin logger.
+ *
+ * @return SLF4J logger instance
+ */
+ @NotNull
+ Logger getLogger();
+
+ /**
+ * Returns the configured {@link MiniMessage} instance with all permitted color and format
+ * tag resolvers registered.
+ *
+ * @return the MiniMessage instance
+ */
+ @NotNull
+ MiniMessage getMiniMessage();
+
+ /**
+ * Returns the {@link ConfigProvider} for {@code config.yml}.
+ *
+ * @return config provider
+ */
+ @NotNull
+ ConfigProvider getConfigProvider();
+
+ /**
+ * Returns the {@link ConfigProvider} for {@code locale.yml}.
+ *
+ * @return locale provider
+ */
+ @NotNull
+ ConfigProvider getLocaleProvider();
+}
diff --git a/core/src/main/java/simplexity/simplenicks/platform/PlayerInfo.java b/core/src/main/java/simplexity/simplenicks/platform/PlayerInfo.java
new file mode 100644
index 0000000..9f2cd21
--- /dev/null
+++ b/core/src/main/java/simplexity/simplenicks/platform/PlayerInfo.java
@@ -0,0 +1,18 @@
+package simplexity.simplenicks.platform;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+/**
+ * Immutable snapshot of a player's identity and last login time.
+ *
+ * Replaces {@code OfflinePlayer} in core logic so that player data can be
+ * resolved purely from the database without platform API calls.
+ *
+ *
+ * @param uuid the player's UUID
+ * @param username the player's last known username
+ * @param lastLoginMillis epoch milliseconds of the player's last login, or {@code -1} if unknown
+ */
+public record PlayerInfo(@NotNull UUID uuid, @NotNull String username, long lastLoginMillis) {}
diff --git a/core/src/main/java/simplexity/simplenicks/platform/SenderContext.java b/core/src/main/java/simplexity/simplenicks/platform/SenderContext.java
new file mode 100644
index 0000000..b01dd8b
--- /dev/null
+++ b/core/src/main/java/simplexity/simplenicks/platform/SenderContext.java
@@ -0,0 +1,56 @@
+package simplexity.simplenicks.platform;
+
+import net.kyori.adventure.text.Component;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Optional;
+import java.util.UUID;
+
+/**
+ * Platform-agnostic abstraction for a command sender (player or console).
+ *
+ * Paper wraps {@code CommandSender}; Fabric wraps {@code CommandSourceStack}.
+ * Core logic uses this interface instead of platform types for permission checks
+ * and message delivery.
+ *
+ */
+public interface SenderContext {
+
+ /**
+ * Checks whether this sender has the given permission node.
+ *
+ * @param permission the permission node string
+ * @return {@code true} if the sender has the permission
+ */
+ boolean hasPermission(@NotNull String permission);
+
+ /**
+ * Returns the UUID of this sender if they are a player, otherwise empty.
+ *
+ * @return the player's UUID, or empty for the console
+ */
+ @NotNull
+ Optional getUuid();
+
+ /**
+ * Sends an Adventure component message to this sender.
+ *
+ * @param message the component to send
+ */
+ void sendMessage(@NotNull Component message);
+
+ /**
+ * Returns whether this sender is a player (as opposed to the console or a command block).
+ *
+ * @return {@code true} if the sender is a player
+ */
+ boolean isPlayer();
+
+ /**
+ * Returns a display-friendly name for this sender (player name or "[Console]").
+ *
+ * @return the sender's display name
+ */
+ @NotNull
+ String getDisplayName();
+}
diff --git a/src/main/java/simplexity/simplenicks/saving/Cache.java b/core/src/main/java/simplexity/simplenicks/saving/Cache.java
similarity index 92%
rename from src/main/java/simplexity/simplenicks/saving/Cache.java
rename to core/src/main/java/simplexity/simplenicks/saving/Cache.java
index 9cce695..96bc431 100644
--- a/src/main/java/simplexity/simplenicks/saving/Cache.java
+++ b/core/src/main/java/simplexity/simplenicks/saving/Cache.java
@@ -1,13 +1,11 @@
package simplexity.simplenicks.saving;
-import net.kyori.adventure.text.minimessage.MiniMessage;
-import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
-import simplexity.simplenicks.SimpleNicks;
+import simplexity.simplenicks.SimpleNicksCore;
import simplexity.simplenicks.config.ConfigHandler;
-import javax.annotation.Nullable;
+import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -39,7 +37,6 @@
*/
public class Cache {
private static Cache instance;
- private static final Logger logger = SimpleNicks.getInstance().getSLF4JLogger();
public Cache() {
}
@@ -49,7 +46,10 @@ public static Cache getInstance() {
return instance;
}
- private final MiniMessage miniMessage = SimpleNicks.getMiniMessage();
+ private static Logger logger() {
+ return SimpleNicksCore.get().platform().getLogger();
+ }
+
private final HashMap activeNicknames = new HashMap<>();
private final HashMap> savedNicknames = new HashMap<>();
@@ -59,7 +59,6 @@ public static Cache getInstance() {
* @param uuid player UUID
* @see SqlHandler#getCurrentNicknameForPlayer(UUID)
*/
-
public void loadCurrentNickname(@NotNull UUID uuid) {
debug("Loading current nickname for UUID %s", uuid);
Nickname currentNick = SqlHandler.getInstance().getCurrentNicknameForPlayer(uuid);
@@ -123,10 +122,10 @@ public Nickname getActiveNickname(@NotNull UUID uuid) {
public List getSavedNicknames(@NotNull UUID uuid) {
debug("Getting saved nicknames for UUID %s", uuid);
if (savedNicknames.containsKey(uuid)) {
- debug("Saved nicknames exists for UUID %s, Nicknames:", uuid, savedNicknames.get(uuid));
+ debug("Saved nicknames exists for UUID %s", uuid);
return savedNicknames.get(uuid);
}
- debug("No Saved Nicknames for UUID %s", uuid);
+ debug("No saved nicknames for UUID %s", uuid);
return new ArrayList<>();
}
@@ -145,10 +144,9 @@ public List getSavedNicknames(@NotNull UUID uuid) {
* @see SqlHandler#setActiveNickname(UUID, String, String, String)
* @see SqlHandler#updatePlayerTable(UUID, String)
*/
-
public boolean setActiveNickname(@NotNull UUID uuid, @NotNull String username, @NotNull String nickname) {
debug("Setting active nickname '%s' for UUID %s", nickname, uuid);
- String normalizedNick = miniMessage.stripTags(nickname).toLowerCase();
+ String normalizedNick = SimpleNicksCore.get().miniMessage().stripTags(nickname).toLowerCase();
Nickname nick = new Nickname(nickname, normalizedNick);
boolean sqlActiveNameSet = SqlHandler.getInstance().setActiveNickname(uuid, username, nickname, normalizedNick);
if (!sqlActiveNameSet) {
@@ -175,7 +173,6 @@ public boolean setActiveNickname(@NotNull UUID uuid, @NotNull String username, @
* @return {@code true} if the nickname was deleted; {@code false} if it didn't exist or SQL failed
* @see SqlHandler#deleteNickname(UUID, String)
*/
-
public boolean deleteSavedNickname(@NotNull UUID uuid, @NotNull String nickname) {
debug("Deleting saved nickname '%s' for UUID %s", nickname, uuid);
boolean sqlDeleted = SqlHandler.getInstance().deleteNickname(uuid, nickname);
@@ -227,11 +224,10 @@ public boolean clearCurrentNickname(@NotNull UUID uuid) {
/**
* Resolves all UUIDs of players who currently use a given normalized nickname.
*
- * @param nickname normalized nickname (tags stripped & lowercased)
+ * @param nickname normalized nickname (tags stripped & lowercased)
* @return list of UUIDs; empty if none found or SQL error
* @see SqlHandler#getUuidsOfNickname(String)
*/
-
@NotNull
public List getUuidOfNormalizedName(@NotNull String nickname) {
debug("Getting UUIDs for normalized nickname '%s'", nickname);
@@ -279,7 +275,7 @@ public boolean nickInUseOnlinePlayers(@Nullable UUID uuid, @NotNull String norma
*/
public boolean saveNickname(@NotNull UUID uuid, @NotNull String username, @NotNull String nickname) {
debug("Saving nickname '%s' for UUID %s", nickname, uuid);
- String normalized = miniMessage.stripTags(nickname).toLowerCase();
+ String normalized = SimpleNicksCore.get().miniMessage().stripTags(nickname).toLowerCase();
Nickname nick = new Nickname(nickname, normalized);
List userSavedNicknames = getSavedNicknames(uuid);
userSavedNicknames.add(nick);
@@ -297,7 +293,6 @@ public boolean saveNickname(@NotNull UUID uuid, @NotNull String username, @NotNu
return true;
}
-
/**
* Gets the number of saved nicknames for a player in the cache.
*
@@ -335,21 +330,18 @@ public void removePlayerFromCache(@NotNull UUID uuid) {
* @return unmodifiable map of UUID → active nickname
*/
@NotNull
- public Map getOnlineNicknames(){
+ public Map getOnlineNicknames() {
debug("Getting all online nicknames from cache");
return Collections.unmodifiableMap(activeNicknames);
}
- private boolean playerIsOffline(@NotNull UUID uuid){
- return Bukkit.getPlayer(uuid) == null;
+ private boolean playerIsOffline(@NotNull UUID uuid) {
+ return !SimpleNicksCore.get().platform().isPlayerOnline(uuid);
}
private void debug(@NotNull String message, @Nullable Object... args) {
if (ConfigHandler.getInstance().isDebugMode()) {
- String messageToSend = String.format(message, args);
- logger.info("[CACHE DEBUG] {}", messageToSend);
+ logger().info("[CACHE DEBUG] " + String.format(message, args));
}
}
-
-
}
diff --git a/src/main/java/simplexity/simplenicks/saving/Nickname.java b/core/src/main/java/simplexity/simplenicks/saving/Nickname.java
similarity index 100%
rename from src/main/java/simplexity/simplenicks/saving/Nickname.java
rename to core/src/main/java/simplexity/simplenicks/saving/Nickname.java
diff --git a/src/main/java/simplexity/simplenicks/saving/NicknameRecord.java b/core/src/main/java/simplexity/simplenicks/saving/NicknameRecord.java
similarity index 100%
rename from src/main/java/simplexity/simplenicks/saving/NicknameRecord.java
rename to core/src/main/java/simplexity/simplenicks/saving/NicknameRecord.java
diff --git a/src/main/java/simplexity/simplenicks/saving/SqlHandler.java b/core/src/main/java/simplexity/simplenicks/saving/SqlHandler.java
similarity index 85%
rename from src/main/java/simplexity/simplenicks/saving/SqlHandler.java
rename to core/src/main/java/simplexity/simplenicks/saving/SqlHandler.java
index 8ba1740..2bdf12a 100644
--- a/src/main/java/simplexity/simplenicks/saving/SqlHandler.java
+++ b/core/src/main/java/simplexity/simplenicks/saving/SqlHandler.java
@@ -4,15 +4,18 @@
import com.zaxxer.hikari.HikariDataSource;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
-import simplexity.simplenicks.SimpleNicks;
+import simplexity.simplenicks.SimpleNicksCore;
import simplexity.simplenicks.config.ConfigHandler;
-import javax.annotation.Nullable;
+import org.jetbrains.annotations.Nullable;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
import java.util.ArrayList;
+import java.util.Date;
import java.util.List;
import java.util.UUID;
@@ -41,7 +44,7 @@ private SqlHandler() {
}
/**
- * Returns the instance of the SQL handler
+ * Returns the instance of the SQL handler.
*
* @return SqlHandler
*/
@@ -53,8 +56,16 @@ public static SqlHandler getInstance() {
private static final HikariConfig hikariConfig = new HikariConfig();
private static HikariDataSource dataSource;
- private final Logger logger = SimpleNicks.getInstance().getSLF4JLogger();
private static final int SCHEMA_VERSION = 1;
+ private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ static {
+ // SQLite CURRENT_TIMESTAMP stores UTC; parse accordingly regardless of JVM timezone
+ DATE_FORMAT.setTimeZone(java.util.TimeZone.getTimeZone("UTC"));
+ }
+
+ private static Logger logger() {
+ return SimpleNicksCore.get().platform().getLogger();
+ }
/**
* Initializes the database, creating tables if they do not exist
@@ -120,7 +131,7 @@ REFERENCES players(uuid)
""");
currentNickStatement.execute();
} catch (SQLException e) {
- logger.warn("Issue connecting to database: {} ", e.getMessage(), e);
+ logger().warn("Issue connecting to database: {} ", e.getMessage(), e);
}
}
@@ -133,8 +144,7 @@ REFERENCES players(uuid)
*/
@Nullable
public List nickAlreadySavedTo(@Nullable UUID uuidToExclude, @NotNull String normalized) {
- debug("Checking if nickname '{}' is already in use (excluding UUID='{}')", normalized,
- uuidToExclude);
+ debug("Checking if nickname '{}' is already in use (excluding UUID='{}')", normalized, uuidToExclude);
String queryString = "SELECT uuid FROM current_nicknames WHERE nickname = ?";
List uuidsWithName = new ArrayList<>();
try (Connection connection = getConnection()) {
@@ -149,7 +159,7 @@ public List nickAlreadySavedTo(@Nullable UUID uuidToExclude, @NotNull Stri
}
return uuidsWithName;
} catch (SQLException e) {
- logger.warn("Failed to check if nickname exists: {}", normalized, e);
+ logger().warn("Failed to check if nickname exists: {}", normalized, e);
return null;
}
}
@@ -178,7 +188,7 @@ public List getSavedNicknamesForPlayer(@NotNull UUID uuid) {
savedNicknames.add(nick);
}
} catch (SQLException e) {
- logger.warn("Failed to get saved nicknames for player with UUID: {}", uuid, e);
+ logger().warn("Failed to get saved nicknames for player with UUID: {}", uuid, e);
return null;
}
return savedNicknames;
@@ -191,7 +201,6 @@ public List getSavedNicknamesForPlayer(@NotNull UUID uuid) {
* @param nickname the nickname string to check
* @return true if the nickname is already saved, false otherwise
*/
-
public boolean userAlreadySavedThisName(@NotNull UUID uuid, @NotNull String nickname) {
debug("Checking if UUID={} already saved nickname='{}'", uuid, nickname);
if (!playerSaveExists(uuid)) return false;
@@ -203,12 +212,11 @@ public boolean userAlreadySavedThisName(@NotNull UUID uuid, @NotNull String nick
ResultSet resultSet = statement.executeQuery();
return resultSet.next();
} catch (SQLException e) {
- logger.warn("Failed to check if UUID '{}' has already saved the nickname '{}'", uuid, nickname, e);
+ logger().warn("Failed to check if UUID '{}' has already saved the nickname '{}'", uuid, nickname, e);
return false;
}
}
-
/**
* Gets the player's currently active nickname.
*
@@ -232,10 +240,9 @@ public Nickname getCurrentNicknameForPlayer(@NotNull UUID uuid) {
}
return null;
} catch (SQLException e) {
- logger.warn("Failed to get active nickname for UUID: {}", uuid, e);
+ logger().warn("Failed to get active nickname for UUID: {}", uuid, e);
return null;
}
-
}
/**
@@ -260,7 +267,7 @@ public List getUuidsOfNickname(@NotNull String normalized) {
}
return uuids;
} catch (SQLException e) {
- logger.warn("Failed to get UUID list from normalized nickname: {}", normalized, e);
+ logger().warn("Failed to get UUID list from normalized nickname: {}", normalized, e);
return null;
}
}
@@ -268,15 +275,14 @@ public List getUuidsOfNickname(@NotNull String normalized) {
/**
* Saves a nickname for a player in the database.
*
- * @param uuid the player's UUID
- * @param username the last known username
- * @param nickname the chosen nickname
+ * @param uuid the player's UUID
+ * @param username the last known username
+ * @param nickname the chosen nickname
* @param normalized normalized form of the nickname
* @return true if the nickname was saved successfully, false otherwise
*/
public boolean saveNickname(@NotNull UUID uuid, @NotNull String username, @NotNull String nickname, @NotNull String normalized) {
- debug("Saving nickname '{}' (normalized '{}') for UUID={}, username={}", nickname,
- normalized, uuid, username);
+ debug("Saving nickname '{}' (normalized '{}') for UUID={}, username={}", nickname, normalized, uuid, username);
String saveString = "REPLACE INTO saved_nicknames (uuid, nickname, normalized) VALUES (?, ?, ?)";
if (!playerSaveExists(uuid)) {
if (!updatePlayerTable(uuid, username)) return false;
@@ -290,13 +296,12 @@ public boolean saveNickname(@NotNull UUID uuid, @NotNull String username, @NotNu
debug("Rows modified when saving nickname: {}", rowsChanged);
return rowsChanged > 0;
} catch (SQLException e) {
- logger.warn("Failed to save nickname '{}' for UUID '{}'. Normalized nickname: {}, Username: {} ",
+ logger().warn("Failed to save nickname '{}' for UUID '{}'. Normalized nickname: {}, Username: {} ",
nickname, uuid, normalized, username, e);
return false;
}
}
-
/**
* Deletes a saved nickname for a player.
*
@@ -316,19 +321,17 @@ public boolean deleteNickname(@NotNull UUID uuid, @NotNull String nickname) {
debug("Rows affected in delete: {}", rowsChanged);
return rowsChanged > 0;
} catch (SQLException e) {
- logger.warn("Failed to delete nickname '{}' for UUID '{}'", nickname, uuid, e);
+ logger().warn("Failed to delete nickname '{}' for UUID '{}'", nickname, uuid, e);
return false;
}
}
-
/**
* Clears the player's currently active nickname.
*
* @param uuid the player's UUID
* @return true if a nickname was cleared, false otherwise
*/
-
public boolean clearActiveNickname(@NotNull UUID uuid) {
debug("Clearing current nickname for UUID={}", uuid);
if (!playerSaveExists(uuid)) return false;
@@ -340,7 +343,7 @@ public boolean clearActiveNickname(@NotNull UUID uuid) {
debug("Rows affected in clear: {}", rowsAffected);
return rowsAffected > 0;
} catch (SQLException e) {
- logger.warn("Failed to clear active nickname for UUID '{}'", uuid, e);
+ logger().warn("Failed to clear active nickname for UUID '{}'", uuid, e);
return false;
}
}
@@ -354,11 +357,9 @@ public boolean clearActiveNickname(@NotNull UUID uuid) {
* @param normalized normalized form of the nickname
* @return true if the nickname was set, false otherwise
*/
-
public boolean setActiveNickname(@NotNull UUID uuid, @NotNull String username, @NotNull String nickname,
@NotNull String normalized) {
- debug("Setting active nickname '{}' (normalized '{}') for UUID={}, username={}", nickname,
- normalized, uuid, username);
+ debug("Setting active nickname '{}' (normalized '{}') for UUID={}, username={}", nickname, normalized, uuid, username);
String setQuery = "REPLACE INTO current_nicknames (uuid, nickname, normalized) VALUES (?, ?, ?)";
if (!playerSaveExists(uuid)) {
if (!updatePlayerTable(uuid, username)) return false;
@@ -372,21 +373,18 @@ public boolean setActiveNickname(@NotNull UUID uuid, @NotNull String username, @
debug("Rows affected setting active nickname: {}", rowsModified);
return rowsModified > 0;
} catch (SQLException e) {
- logger.warn("Failed to set active nickname '{}' for UUID '{}'. Normalized name: {}, Username: {}",
+ logger().warn("Failed to set active nickname '{}' for UUID '{}'. Normalized name: {}, Username: {}",
nickname, uuid, normalized, username, e);
- e.printStackTrace();
return false;
}
}
-
/**
* Checks if the player exists in the database.
*
* @param uuid the player's UUID
* @return true if the player exists, false otherwise
*/
-
public boolean playerSaveExists(@NotNull UUID uuid) {
String queryString = "SELECT 1 FROM players WHERE uuid = ? LIMIT 1";
try (Connection connection = getConnection()) {
@@ -394,11 +392,10 @@ public boolean playerSaveExists(@NotNull UUID uuid) {
statement.setString(1, String.valueOf(uuid));
ResultSet resultSet = statement.executeQuery();
boolean exists = resultSet.next();
- debug("Check if player UUID=%s exists: %s", uuid, exists);
+ debug("Check if player UUID={} exists: {}", uuid, exists);
return exists;
} catch (SQLException e) {
- logger.warn("Failed to check if player with UUID {} exists", uuid, e);
- e.printStackTrace();
+ logger().warn("Failed to check if player with UUID {} exists", uuid, e);
return false;
}
}
@@ -410,10 +407,9 @@ public boolean playerSaveExists(@NotNull UUID uuid) {
* @param expiryTime maximum age in milliseconds (negative for no limit)
* @return last login time, or {@code null} if not found
*/
-
@Nullable
public Long lastLoginOfUsername(@NotNull String username, long expiryTime) {
- debug("Fetching last login for username='%s', expiry=%g", username, expiryTime);
+ debug("Fetching last login for username='{}', expiry={}", username, expiryTime);
String queryString = "SELECT last_login FROM players WHERE last_known_name = ? AND (? < 0 OR last_login >= ?)";
try (Connection connection = getConnection()) {
PreparedStatement statement = connection.prepareStatement(queryString);
@@ -425,7 +421,7 @@ public Long lastLoginOfUsername(@NotNull String username, long expiryTime) {
if (resultSet.next()) return resultSet.getLong("last_login");
return null;
} catch (SQLException e) {
- logger.warn("Failed to get last login for username: {}, expiry time: {}", username, expiryTime, e);
+ logger().warn("Failed to get last login for username: {}, expiry time: {}", username, expiryTime, e);
return null;
}
}
@@ -439,7 +435,7 @@ public Long lastLoginOfUsername(@NotNull String username, long expiryTime) {
*/
@Nullable
public Long lastLoginOfUuid(@NotNull UUID uuid, long expiryTime) {
- debug("Fetching last login for UUID='%s', expiry=%g", uuid, expiryTime);
+ debug("Fetching last login for UUID='{}', expiry={}", uuid, expiryTime);
String queryString = "SELECT last_login FROM players WHERE uuid = ? AND (? < 0 OR last_login >= ?)";
try (Connection connection = getConnection()) {
PreparedStatement statement = connection.prepareStatement(queryString);
@@ -451,11 +447,41 @@ public Long lastLoginOfUuid(@NotNull UUID uuid, long expiryTime) {
if (resultSet.next()) return resultSet.getLong("last_login");
return null;
} catch (SQLException e) {
- logger.warn("Failed to get last login for UUID: {}, with expiry time: {}", uuid, expiryTime, e);
+ logger().warn("Failed to get last login for UUID: {}, with expiry time: {}", uuid, expiryTime, e);
return null;
}
}
+ /**
+ * Returns the last login time of a player as epoch milliseconds, parsed from the
+ * {@code last_login} DATETIME column stored as {@code "yyyy-MM-dd HH:mm:ss"}.
+ *
+ * @param uuid the player's UUID
+ * @return epoch milliseconds of last login, or {@code -1} if not found or unparseable
+ */
+ public long getLastLoginMillis(@NotNull UUID uuid) {
+ debug("Fetching last_login millis for UUID={}", uuid);
+ String queryString = "SELECT last_login FROM players WHERE uuid = ? LIMIT 1";
+ try (Connection connection = getConnection()) {
+ PreparedStatement statement = connection.prepareStatement(queryString);
+ statement.setString(1, String.valueOf(uuid));
+ ResultSet resultSet = statement.executeQuery();
+ if (resultSet.next()) {
+ String raw = resultSet.getString("last_login");
+ if (raw == null) return -1;
+ try {
+ Date parsed = DATE_FORMAT.parse(raw);
+ return parsed.getTime();
+ } catch (ParseException e) {
+ logger().warn("Could not parse last_login '{}' for UUID {}", raw, uuid);
+ }
+ }
+ } catch (SQLException e) {
+ logger().warn("Failed to get last_login millis for UUID: {}", uuid, e);
+ }
+ return -1;
+ }
+
/**
* Inserts or updates a player in the players table.
*
@@ -465,7 +491,7 @@ public Long lastLoginOfUuid(@NotNull UUID uuid, long expiryTime) {
*/
@SuppressWarnings("ExtractMethodRecommender")
public boolean updatePlayerTable(@NotNull UUID uuid, @NotNull String username) {
- debug("Saving player to players table: UUID=%s, username=%g", uuid, username);
+ debug("Saving player to players table: UUID={}, username={}", uuid, username);
boolean isMySql = ConfigHandler.getInstance().isMySql();
String insertQuery;
if (isMySql) {
@@ -490,10 +516,10 @@ ON CONFLICT(uuid) DO UPDATE SET
statement.setString(1, String.valueOf(uuid));
statement.setString(2, username.toLowerCase());
int rowsChanged = statement.executeUpdate();
- debug("Rows affected in savePlayerToPlayers: %d", rowsChanged);
+ debug("Rows affected in updatePlayerTable: {}", rowsChanged);
return rowsChanged > 0;
} catch (SQLException e) {
- logger.warn("Failed to save player with UUID: {}, username: {}", uuid, username, e);
+ logger().warn("Failed to save player with UUID: {}, username: {}", uuid, username, e);
return false;
}
}
@@ -534,7 +560,7 @@ public boolean batchInsertNicknames(@NotNull List records) {
for (NicknameRecord record : records) {
addPlayerStatement.setString(1, record.uuid().toString());
addPlayerStatement.setString(2, record.username());
- addPlayerStatement.setString(3, new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date(record.lastLogin())));
+ addPlayerStatement.setString(3, DATE_FORMAT.format(new Date(record.lastLogin())));
addPlayerStatement.addBatch();
if (record.isActiveNickname()) {
@@ -565,14 +591,14 @@ public boolean batchInsertNicknames(@NotNull List records) {
savedNicknameStatement.executeBatch();
connection.commit();
} catch (SQLException e) {
- logger.error("Error migrating nickname records", e);
+ logger().error("Error migrating nickname records", e);
return false;
}
return true;
}
/**
- * Closes the database connection pool. Used during reloads or shutdown
+ * Closes the database connection pool. Used during reloads or shutdown.
*/
public void closeDatabase() {
if (dataSource != null && !dataSource.isClosed()) dataSource.close();
@@ -584,7 +610,7 @@ public void closeDatabase() {
*/
public void setupConfig() {
if (!ConfigHandler.getInstance().isMySql()) {
- hikariConfig.setJdbcUrl("jdbc:sqlite:" + SimpleNicks.getInstance().getDataFolder() + "/simplenicks.db?foreign_keys=on");
+ hikariConfig.setJdbcUrl("jdbc:sqlite:" + SimpleNicksCore.get().platform().getDataDirectory().resolve("simplenicks.db") + "?foreign_keys=on");
hikariConfig.setMaximumPoolSize(10);
hikariConfig.setConnectionTestQuery("PRAGMA journal_mode = WAL;");
dataSource = new HikariDataSource(hikariConfig);
@@ -612,7 +638,7 @@ public int getSchemaVersion() {
return resultSet.getInt("version");
}
} catch (SQLException e) {
- logger.warn("Failed to get schema version", e);
+ logger().warn("Failed to get schema version", e);
}
return -1;
}
@@ -625,7 +651,7 @@ private void runMigrations(Connection connection, int fromVersion) {
updateVersion.executeUpdate();
debug("Schema upgraded to version {}", SCHEMA_VERSION);
} catch (SQLException e) {
- logger.error("Error running schema migrations", e);
+ logger().error("Error running schema migrations", e);
}
}
@@ -636,9 +662,7 @@ private static Connection getConnection() throws SQLException {
private void debug(String message, Object... args) {
if (ConfigHandler.getInstance().isDebugMode()) {
- logger.info("[SQL DEBUG] {}, {}", message, args);
+ logger().info("[SQL DEBUG] " + message, args);
}
}
-
-
}
diff --git a/core/src/main/java/simplexity/simplenicks/util/ColorTag.java b/core/src/main/java/simplexity/simplenicks/util/ColorTag.java
new file mode 100644
index 0000000..afd0ea3
--- /dev/null
+++ b/core/src/main/java/simplexity/simplenicks/util/ColorTag.java
@@ -0,0 +1,37 @@
+package simplexity.simplenicks.util;
+
+import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
+import net.kyori.adventure.text.minimessage.tag.standard.StandardTags;
+import org.jetbrains.annotations.NotNull;
+
+public enum ColorTag {
+ HEX_COLOR("simplenick.color.basic", "op", StandardTags.color()),
+ GRADIENT("simplenick.color.gradient", "op", StandardTags.gradient()),
+ RAINBOW("simplenick.color.rainbow", "op", StandardTags.rainbow()),
+ RESET("simplenick.color.reset", "op", StandardTags.reset());
+
+ private final String permissionKey;
+ private final String permissionDefault;
+ private final TagResolver resolver;
+
+ ColorTag(@NotNull String permissionKey, @NotNull String permissionDefault, @NotNull TagResolver resolver) {
+ this.permissionKey = permissionKey;
+ this.permissionDefault = permissionDefault;
+ this.resolver = resolver;
+ }
+
+ @NotNull
+ public String getPermissionKey() {
+ return permissionKey;
+ }
+
+ @NotNull
+ public String getPermissionDefault() {
+ return permissionDefault;
+ }
+
+ @NotNull
+ public TagResolver getTagResolver() {
+ return resolver;
+ }
+}
diff --git a/core/src/main/java/simplexity/simplenicks/util/FormatTag.java b/core/src/main/java/simplexity/simplenicks/util/FormatTag.java
new file mode 100644
index 0000000..44e1eb8
--- /dev/null
+++ b/core/src/main/java/simplexity/simplenicks/util/FormatTag.java
@@ -0,0 +1,41 @@
+package simplexity.simplenicks.util;
+
+import net.kyori.adventure.text.format.TextDecoration;
+import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
+import net.kyori.adventure.text.minimessage.tag.standard.StandardTags;
+import org.jetbrains.annotations.NotNull;
+
+public enum FormatTag {
+ UNDERLINE("simplenick.format.underline", "op", StandardTags.decorations(TextDecoration.UNDERLINED)),
+ ITALIC("simplenick.format.italic", "op", StandardTags.decorations(TextDecoration.ITALIC)),
+ STRIKETHROUGH("simplenick.format.strikethrough", "op", StandardTags.decorations(TextDecoration.STRIKETHROUGH)),
+ BOLD("simplenick.format.bold", "op", StandardTags.decorations(TextDecoration.BOLD)),
+ OBFUSCATED("simplenick.format.obfuscated", "op", StandardTags.decorations(TextDecoration.OBFUSCATED)),
+ HOVER("simplenick.format.hover", "false", StandardTags.hoverEvent()),
+ FONT("simplenick.format.font", "false", StandardTags.font());
+
+ private final String permissionKey;
+ private final String permissionDefault;
+ private final TagResolver resolver;
+
+ FormatTag(@NotNull String permissionKey, @NotNull String permissionDefault, @NotNull TagResolver resolver) {
+ this.permissionKey = permissionKey;
+ this.permissionDefault = permissionDefault;
+ this.resolver = resolver;
+ }
+
+ @NotNull
+ public String getPermissionKey() {
+ return permissionKey;
+ }
+
+ @NotNull
+ public String getPermissionDefault() {
+ return permissionDefault;
+ }
+
+ @NotNull
+ public TagResolver getTagResolver() {
+ return resolver;
+ }
+}
diff --git a/core/src/main/java/simplexity/simplenicks/util/NickPermission.java b/core/src/main/java/simplexity/simplenicks/util/NickPermission.java
new file mode 100644
index 0000000..4a94655
--- /dev/null
+++ b/core/src/main/java/simplexity/simplenicks/util/NickPermission.java
@@ -0,0 +1,44 @@
+package simplexity.simplenicks.util;
+
+import org.jetbrains.annotations.NotNull;
+
+public enum NickPermission {
+ NICK_ADMIN("simplenick.admin", "op"),
+ NICK_ADMIN_SET("simplenick.admin.set", "op"),
+ NICK_ADMIN_RESET("simplenick.admin.reset", "op"),
+ NICK_ADMIN_DELETE("simplenick.admin.delete", "op"),
+ NICK_ADMIN_LOOKUP("simplenick.admin.lookup", "op"),
+ NICK_COMMAND("simplenick.nick", "true"),
+ NICK_SET("simplenick.nick.set", "op"),
+ NICK_SAVE("simplenick.nick.save", "op"),
+ NICK_WHO("simplenick.nick.who", "true"),
+ NICK_HELP("simplenick.nick.help", "true"),
+ NICK_BYPASS_USERNAME("simplenick.bypass.username", "false"),
+ NICK_BYPASS_LENGTH("simplenick.bypass.length", "false"),
+ NICK_BYPASS_REGEX("simplenick.bypass.regex", "false"),
+ NICK_BYPASS_NICK_PROTECTION("simplenick.bypass.nick-protection", "false"),
+ NICK_RELOAD("simplenick.reload", "op");
+
+ private final String permissionKey;
+ private final String permissionDefault;
+
+ NickPermission(@NotNull String permissionKey, @NotNull String permissionDefault) {
+ this.permissionKey = permissionKey;
+ this.permissionDefault = permissionDefault;
+ }
+
+ @NotNull
+ public String getPermissionKey() {
+ return permissionKey;
+ }
+
+ /**
+ * Returns the default grant level: {@code "op"}, {@code "true"}, or {@code "false"}.
+ *
+ * @return the permission default string
+ */
+ @NotNull
+ public String getPermissionDefault() {
+ return permissionDefault;
+ }
+}
diff --git a/fabric/build.gradle b/fabric/build.gradle
new file mode 100644
index 0000000..5b4572a
--- /dev/null
+++ b/fabric/build.gradle
@@ -0,0 +1,33 @@
+plugins {
+ id 'net.fabricmc.fabric-loom' version '1.16-SNAPSHOT'
+}
+
+java {
+ targetCompatibility = JavaVersion.VERSION_25
+ sourceCompatibility = JavaVersion.VERSION_25
+}
+
+tasks.withType(JavaCompile).configureEach {
+ options.release = 25
+}
+
+tasks.named('jar') {
+ archiveFileName = "SimpleNicks-fabric-${project.version}.jar"
+}
+
+dependencies {
+ minecraft "com.mojang:minecraft:${fabricMinecraftVersion}"
+ implementation "net.fabricmc:fabric-loader:${fabricLoaderVersion}"
+ implementation "net.fabricmc.fabric-api:fabric-api:${fabricApiVersion}"
+
+ include(implementation(project(':core')))
+
+ include(implementation("net.kyori:adventure-platform-fabric:${adventureFabricVersion}"))
+ include(implementation("net.kyori:adventure-text-minimessage:${fabricMiniMessageVersion}"))
+ include(implementation("com.zaxxer:HikariCP:${hikariVersion}"))
+ include(implementation("org.xerial:sqlite-jdbc:3.47.+"))
+ include(implementation("org.yaml:snakeyaml:2.2"))
+
+ // fabric-permissions-api: softdepend permissions with op-level fallback
+ include(implementation("me.lucko:fabric-permissions-api:${fabricPermissionsVersion}"))
+}
diff --git a/fabric/src/main/java/simplexity/simplenicks/fabric/SimpleNicksFabric.java b/fabric/src/main/java/simplexity/simplenicks/fabric/SimpleNicksFabric.java
new file mode 100644
index 0000000..9a2ef53
--- /dev/null
+++ b/fabric/src/main/java/simplexity/simplenicks/fabric/SimpleNicksFabric.java
@@ -0,0 +1,44 @@
+package simplexity.simplenicks.fabric;
+
+import net.fabricmc.api.ModInitializer;
+import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
+import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
+import net.fabricmc.loader.api.FabricLoader;
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.config.ConfigHandler;
+import simplexity.simplenicks.fabric.commands.FabricNicknameCommand;
+import simplexity.simplenicks.fabric.events.FabricLeaveHandler;
+import simplexity.simplenicks.fabric.events.FabricLoginHandler;
+import simplexity.simplenicks.fabric.platform.FabricPlatformAdapter;
+import simplexity.simplenicks.saving.SqlHandler;
+
+import java.nio.file.Path;
+
+/**
+ * Fabric mod entry point for SimpleNicks.
+ */
+public class SimpleNicksFabric implements ModInitializer {
+
+ @Override
+ public void onInitialize() {
+ Path dataDir = FabricLoader.getInstance().getConfigDir().resolve("simplenicks");
+
+ ServerLifecycleEvents.SERVER_STARTING.register(server -> {
+ FabricPlatformAdapter adapter = new FabricPlatformAdapter(server, dataDir);
+ SimpleNicksCore.initialize(adapter);
+ ConfigHandler.getInstance().reloadConfig();
+ SqlHandler.getInstance().init();
+ });
+
+ ServerLifecycleEvents.SERVER_STOPPED.register(server -> {
+ SqlHandler.getInstance().closeDatabase();
+ SimpleNicksCore.shutdown();
+ });
+
+ FabricLoginHandler.register();
+ FabricLeaveHandler.register();
+
+ CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) ->
+ dispatcher.register(FabricNicknameCommand.createCommand()));
+ }
+}
diff --git a/fabric/src/main/java/simplexity/simplenicks/fabric/commands/FabricNicknameCommand.java b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/FabricNicknameCommand.java
new file mode 100644
index 0000000..d5370ed
--- /dev/null
+++ b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/FabricNicknameCommand.java
@@ -0,0 +1,41 @@
+package simplexity.simplenicks.fabric.commands;
+
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import simplexity.simplenicks.fabric.util.FabricPermissions;
+import net.minecraft.commands.CommandSourceStack;
+import net.minecraft.commands.Commands;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.config.ConfigHandler;
+import simplexity.simplenicks.fabric.commands.subcommands.FabricDeleteSubCommand;
+import simplexity.simplenicks.fabric.commands.subcommands.FabricHelpSubCommand;
+import simplexity.simplenicks.fabric.commands.subcommands.FabricReloadSubCommand;
+import simplexity.simplenicks.fabric.commands.subcommands.FabricResetSubCommand;
+import simplexity.simplenicks.fabric.commands.subcommands.FabricSaveSubCommand;
+import simplexity.simplenicks.fabric.commands.subcommands.FabricSetSubCommand;
+import simplexity.simplenicks.fabric.commands.subcommands.FabricWhoSubCommand;
+import simplexity.simplenicks.fabric.commands.subcommands.admin.FabricAdminSubCommand;
+import simplexity.simplenicks.util.NickPermission;
+
+/**
+ * Builds the {@code /nick} command tree for the Fabric platform.
+ * Registered via {@code CommandRegistrationCallback} in
+ * {@link simplexity.simplenicks.fabric.SimpleNicksFabric}.
+ */
+public class FabricNicknameCommand {
+
+ @NotNull
+ public static LiteralArgumentBuilder createCommand() {
+ LiteralArgumentBuilder builder = Commands.literal("nick")
+ .requires(src -> !ConfigHandler.getInstance().isNickRequiresPermission()
+ || FabricPermissions.check(src, NickPermission.NICK_COMMAND));
+ new FabricHelpSubCommand().subcommandTo(builder);
+ new FabricSetSubCommand().subcommandTo(builder);
+ new FabricSaveSubCommand().subcommandTo(builder);
+ new FabricResetSubCommand().subcommandTo(builder);
+ new FabricDeleteSubCommand().subcommandTo(builder);
+ new FabricAdminSubCommand().subcommandTo(builder);
+ new FabricReloadSubCommand().subcommandTo(builder);
+ new FabricWhoSubCommand().subcommandTo(builder);
+ return builder;
+ }
+}
diff --git a/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/FabricDeleteSubCommand.java b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/FabricDeleteSubCommand.java
new file mode 100644
index 0000000..c6eceb9
--- /dev/null
+++ b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/FabricDeleteSubCommand.java
@@ -0,0 +1,67 @@
+package simplexity.simplenicks.fabric.commands.subcommands;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.arguments.StringArgumentType;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import simplexity.simplenicks.fabric.util.FabricPermissions;
+import net.minecraft.commands.CommandSourceStack;
+import net.minecraft.commands.Commands;
+import net.minecraft.server.level.ServerPlayer;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.commands.NicknameProcessor;
+import simplexity.simplenicks.config.LocaleMessage;
+import simplexity.simplenicks.logic.NickUtils;
+import simplexity.simplenicks.saving.Nickname;
+import simplexity.simplenicks.util.NickPermission;
+
+public class FabricDeleteSubCommand implements FabricSubCommand {
+
+ @Override
+ public void subcommandTo(@NotNull LiteralArgumentBuilder parent) {
+ parent.then(Commands.literal("delete").requires(this::canExecute)
+ .then(Commands.argument("nickname", StringArgumentType.greedyString())
+ .suggests((ctx, builder) -> {
+ ServerPlayer player = ctx.getSource().getPlayer();
+ if (player == null) return builder.buildFuture();
+ NicknameProcessor.getInstance()
+ .getSavedNicknames(player.getUUID(), true)
+ .forEach(n -> builder.suggest(n.getNickname()));
+ return builder.buildFuture();
+ })
+ .executes(this::execute)
+ )
+ );
+ }
+
+ @Override
+ public int execute(@NotNull CommandContext ctx) throws CommandSyntaxException {
+ ServerPlayer player = ctx.getSource().getPlayer();
+ if (player == null) return Command.SINGLE_SUCCESS;
+ String raw = StringArgumentType.getString(ctx, "nickname");
+ Nickname nickname = new Nickname(raw, NickUtils.normalizeNickname(raw));
+ SimpleNicksCore.get().platform().runAsync(() -> {
+ boolean success = NicknameProcessor.getInstance()
+ .deleteNickname(player.getUUID(), nickname.getNickname());
+ if (success) {
+ SimpleNicksCore.get().platform().runSync(() -> {
+ NickUtils.refreshDisplayName(player.getUUID());
+ sendToPlayer(player, LocaleMessage.DELETE_SELF, nickname);
+ });
+ } else {
+ SimpleNicksCore.get().platform().runSync(() ->
+ sendToPlayer(player, LocaleMessage.ERROR_DELETE_FAILURE, nickname));
+ }
+ });
+ return Command.SINGLE_SUCCESS;
+ }
+
+ @Override
+ public boolean canExecute(@NotNull CommandSourceStack source) {
+ if (source.getPlayer() == null) return false;
+ return permissionNotRequired()
+ || FabricPermissions.check(source, NickPermission.NICK_SAVE);
+ }
+}
diff --git a/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/FabricHelpSubCommand.java b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/FabricHelpSubCommand.java
new file mode 100644
index 0000000..89d5dde
--- /dev/null
+++ b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/FabricHelpSubCommand.java
@@ -0,0 +1,85 @@
+package simplexity.simplenicks.fabric.commands.subcommands;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import simplexity.simplenicks.fabric.util.FabricPermissions;
+import net.kyori.adventure.text.Component;
+import net.minecraft.commands.CommandSourceStack;
+import net.minecraft.commands.Commands;
+import net.minecraft.server.level.ServerPlayer;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.config.ConfigHandler;
+import simplexity.simplenicks.config.LocaleMessage;
+import simplexity.simplenicks.util.NickPermission;
+
+public class FabricHelpSubCommand implements FabricSubCommand {
+
+ @Override
+ public void subcommandTo(@NotNull LiteralArgumentBuilder parent) {
+ parent.then(Commands.literal("help").requires(this::canExecute)
+ .executes(this::execute)
+ );
+ }
+
+ @Override
+ public int execute(@NotNull CommandContext ctx) {
+ sendToSource(ctx.getSource(), buildHelpMessage(ctx.getSource()));
+ return Command.SINGLE_SUCCESS;
+ }
+
+ @NotNull
+ private Component buildHelpMessage(@NotNull CommandSourceStack source) {
+ ConfigHandler config = ConfigHandler.getInstance();
+ Component help = line(LocaleMessage.HELP_HEADER);
+
+ ServerPlayer player = source.getPlayer();
+ if (player != null) {
+ boolean permNotRequired = !config.isNickRequiresPermission();
+
+ if (permNotRequired || FabricPermissions.check(source, NickPermission.NICK_SET)) {
+ help = help.appendNewline().append(line(LocaleMessage.HELP_SET));
+ help = help.appendNewline().append(line(LocaleMessage.HELP_RESET));
+ }
+
+ if (permNotRequired || FabricPermissions.check(source, NickPermission.NICK_SAVE)) {
+ help = help.appendNewline().append(line(LocaleMessage.HELP_SAVE));
+ help = help.appendNewline().append(line(LocaleMessage.HELP_DELETE));
+ }
+ }
+
+ if (!config.isWhoRequiresPermission()
+ || FabricPermissions.check(source, NickPermission.NICK_WHO)) {
+ help = help.appendNewline().append(line(LocaleMessage.HELP_WHO));
+ }
+
+ if (FabricPermissions.check(source, NickPermission.NICK_ADMIN_SET)) {
+ help = help.appendNewline().append(line(LocaleMessage.HELP_ADMIN_SET));
+ }
+ if (FabricPermissions.check(source, NickPermission.NICK_ADMIN_RESET)) {
+ help = help.appendNewline().append(line(LocaleMessage.HELP_ADMIN_RESET));
+ }
+ if (FabricPermissions.check(source, NickPermission.NICK_ADMIN_DELETE)) {
+ help = help.appendNewline().append(line(LocaleMessage.HELP_ADMIN_DELETE));
+ }
+ if (FabricPermissions.check(source, NickPermission.NICK_ADMIN_LOOKUP)) {
+ help = help.appendNewline().append(line(LocaleMessage.HELP_ADMIN_LOOKUP));
+ }
+ if (FabricPermissions.check(source, NickPermission.NICK_RELOAD)) {
+ help = help.appendNewline().append(line(LocaleMessage.HELP_RELOAD));
+ }
+
+ return help;
+ }
+
+ @NotNull
+ private static Component line(@NotNull LocaleMessage msg) {
+ return SimpleNicksCore.get().miniMessage().deserialize(msg.getMessage());
+ }
+
+ @Override
+ public boolean canExecute(@NotNull CommandSourceStack source) {
+ return FabricPermissions.check(source, NickPermission.NICK_HELP);
+ }
+}
diff --git a/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/FabricReloadSubCommand.java b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/FabricReloadSubCommand.java
new file mode 100644
index 0000000..dc36234
--- /dev/null
+++ b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/FabricReloadSubCommand.java
@@ -0,0 +1,41 @@
+package simplexity.simplenicks.fabric.commands.subcommands;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import simplexity.simplenicks.fabric.util.FabricPermissions;
+import net.minecraft.commands.CommandSourceStack;
+import net.minecraft.commands.Commands;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.config.ConfigHandler;
+import simplexity.simplenicks.config.LocaleMessage;
+import simplexity.simplenicks.logic.NickUtils;
+import simplexity.simplenicks.saving.SqlHandler;
+import simplexity.simplenicks.util.NickPermission;
+
+public class FabricReloadSubCommand implements FabricSubCommand {
+
+ @Override
+ public void subcommandTo(@NotNull LiteralArgumentBuilder parent) {
+ parent.then(Commands.literal("reload").requires(this::canExecute)
+ .executes(this::execute)
+ );
+ }
+
+ @Override
+ public int execute(@NotNull CommandContext ctx) {
+ ConfigHandler.getInstance().reloadConfig();
+ SqlHandler.getInstance().closeDatabase();
+ SqlHandler.getInstance().init();
+ SimpleNicksCore.get().platform().getOnlinePlayers()
+ .forEach(NickUtils::refreshDisplayName);
+ sendFeedback(ctx.getSource(), LocaleMessage.CONFIG_RELOADED, null);
+ return Command.SINGLE_SUCCESS;
+ }
+
+ @Override
+ public boolean canExecute(@NotNull CommandSourceStack source) {
+ return FabricPermissions.check(source, NickPermission.NICK_RELOAD);
+ }
+}
diff --git a/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/FabricResetSubCommand.java b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/FabricResetSubCommand.java
new file mode 100644
index 0000000..7fdf7e8
--- /dev/null
+++ b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/FabricResetSubCommand.java
@@ -0,0 +1,51 @@
+package simplexity.simplenicks.fabric.commands.subcommands;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import simplexity.simplenicks.fabric.util.FabricPermissions;
+import net.minecraft.commands.CommandSourceStack;
+import net.minecraft.commands.Commands;
+import net.minecraft.server.level.ServerPlayer;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.commands.NicknameProcessor;
+import simplexity.simplenicks.config.LocaleMessage;
+import simplexity.simplenicks.logic.NickUtils;
+import simplexity.simplenicks.util.NickPermission;
+
+public class FabricResetSubCommand implements FabricSubCommand {
+
+ @Override
+ public void subcommandTo(@NotNull LiteralArgumentBuilder parent) {
+ parent.then(Commands.literal("reset").requires(this::canExecute)
+ .executes(this::execute)
+ );
+ }
+
+ @Override
+ public int execute(@NotNull CommandContext ctx) {
+ ServerPlayer player = ctx.getSource().getPlayer();
+ if (player == null) return Command.SINGLE_SUCCESS;
+ SimpleNicksCore.get().platform().runAsync(() -> {
+ boolean success = NicknameProcessor.getInstance().resetNickname(player.getUUID());
+ if (success) {
+ SimpleNicksCore.get().platform().runSync(() -> {
+ NickUtils.refreshDisplayName(player.getUUID());
+ sendToPlayer(player, LocaleMessage.RESET_SELF, null);
+ });
+ } else {
+ SimpleNicksCore.get().platform().runSync(() ->
+ sendToPlayer(player, LocaleMessage.ERROR_RESET_FAILURE, null));
+ }
+ });
+ return Command.SINGLE_SUCCESS;
+ }
+
+ @Override
+ public boolean canExecute(@NotNull CommandSourceStack source) {
+ if (source.getPlayer() == null) return false;
+ return permissionNotRequired()
+ || FabricPermissions.check(source, NickPermission.NICK_SET);
+ }
+}
diff --git a/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/FabricSaveSubCommand.java b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/FabricSaveSubCommand.java
new file mode 100644
index 0000000..a10f2e3
--- /dev/null
+++ b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/FabricSaveSubCommand.java
@@ -0,0 +1,101 @@
+package simplexity.simplenicks.fabric.commands.subcommands;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.arguments.StringArgumentType;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import simplexity.simplenicks.fabric.util.FabricPermissions;
+import net.minecraft.commands.CommandSourceStack;
+import net.minecraft.commands.Commands;
+import net.minecraft.server.level.ServerPlayer;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.commands.NicknameProcessor;
+import simplexity.simplenicks.commands.subcommands.Exceptions;
+import simplexity.simplenicks.config.ConfigHandler;
+import simplexity.simplenicks.config.LocaleMessage;
+import simplexity.simplenicks.fabric.platform.FabricSenderContext;
+import simplexity.simplenicks.logic.NickUtils;
+import simplexity.simplenicks.saving.Nickname;
+import simplexity.simplenicks.util.NickPermission;
+
+public class FabricSaveSubCommand implements FabricSubCommand {
+
+ @Override
+ public void subcommandTo(@NotNull LiteralArgumentBuilder parent) {
+ parent.then(Commands.literal("save").requires(this::canExecute)
+ .executes(this::execute)
+ .then(Commands.argument("nickname", StringArgumentType.greedyString())
+ .suggests((ctx, builder) -> {
+ ServerPlayer player = ctx.getSource().getPlayer();
+ if (player == null) return builder.buildFuture();
+ NicknameProcessor.getInstance()
+ .getSavedNicknames(player.getUUID(), true)
+ .forEach(n -> builder.suggest(n.getNickname()));
+ return builder.buildFuture();
+ })
+ .executes(this::executeWithArgument)
+ )
+ );
+ }
+
+ @Override
+ public int execute(@NotNull CommandContext ctx) throws CommandSyntaxException {
+ ServerPlayer player = ctx.getSource().getPlayer();
+ if (player == null) return Command.SINGLE_SUCCESS;
+ Nickname nickname = NicknameProcessor.getInstance().getCurrentNickname(player.getUUID(), true);
+ if (nickname == null) throw Exceptions.cannotSave();
+ checkSaveSlots(player);
+ SimpleNicksCore.get().platform().runAsync(() -> {
+ boolean saved = NicknameProcessor.getInstance()
+ .saveNickname(player.getUUID(), player.getGameProfile().name(), nickname.getNickname());
+ if (saved) {
+ SimpleNicksCore.get().platform().runSync(() -> {
+ NickUtils.refreshDisplayName(player.getUUID());
+ sendToPlayer(player, LocaleMessage.SAVE_NICK, nickname);
+ });
+ } else {
+ SimpleNicksCore.get().platform().runSync(() ->
+ sendToPlayer(player, LocaleMessage.ERROR_SAVE_FAILURE, nickname));
+ }
+ });
+ return Command.SINGLE_SUCCESS;
+ }
+
+ private int executeWithArgument(@NotNull CommandContext ctx) throws CommandSyntaxException {
+ ServerPlayer player = ctx.getSource().getPlayer();
+ if (player == null) return Command.SINGLE_SUCCESS;
+ String raw = StringArgumentType.getString(ctx, "nickname");
+ Nickname nickname = new Nickname(raw, NickUtils.normalizeNickname(raw));
+ NickUtils.nicknameChecks(new FabricSenderContext(ctx.getSource()), nickname);
+ checkSaveSlots(player);
+ if (NicknameProcessor.getInstance().playerAlreadySavedThis(player.getUUID(), nickname.getNickname())) {
+ throw Exceptions.alreadySaved();
+ }
+ SimpleNicksCore.get().platform().runAsync(() -> {
+ boolean saved = NicknameProcessor.getInstance()
+ .saveNickname(player.getUUID(), player.getGameProfile().name(), nickname.getNickname());
+ if (saved) {
+ SimpleNicksCore.get().platform().runSync(() ->
+ sendToPlayer(player, LocaleMessage.SAVE_NICK, nickname));
+ } else {
+ SimpleNicksCore.get().platform().runSync(() ->
+ sendToPlayer(player, LocaleMessage.ERROR_SAVE_FAILURE, nickname));
+ }
+ });
+ return Command.SINGLE_SUCCESS;
+ }
+
+ @Override
+ public boolean canExecute(@NotNull CommandSourceStack source) {
+ if (source.getPlayer() == null) return false;
+ return permissionNotRequired()
+ || FabricPermissions.check(source, NickPermission.NICK_SAVE);
+ }
+
+ private void checkSaveSlots(@NotNull ServerPlayer player) throws CommandSyntaxException {
+ int count = NicknameProcessor.getInstance().getCurrentSavedNickCount(player.getUUID(), true);
+ if (count >= ConfigHandler.getInstance().getMaxSaves()) throw Exceptions.tooManySavedNames();
+ }
+}
diff --git a/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/FabricSetSubCommand.java b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/FabricSetSubCommand.java
new file mode 100644
index 0000000..5dc2dd6
--- /dev/null
+++ b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/FabricSetSubCommand.java
@@ -0,0 +1,73 @@
+package simplexity.simplenicks.fabric.commands.subcommands;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.arguments.StringArgumentType;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import simplexity.simplenicks.fabric.util.FabricPermissions;
+import net.minecraft.commands.CommandSourceStack;
+import net.minecraft.commands.Commands;
+import net.minecraft.server.level.ServerPlayer;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.commands.NicknameProcessor;
+import simplexity.simplenicks.commands.subcommands.Exceptions;
+import simplexity.simplenicks.config.LocaleMessage;
+import simplexity.simplenicks.fabric.platform.FabricSenderContext;
+import simplexity.simplenicks.logic.NickUtils;
+import simplexity.simplenicks.saving.Nickname;
+import simplexity.simplenicks.util.NickPermission;
+
+public class FabricSetSubCommand implements FabricSubCommand {
+
+ @Override
+ public void subcommandTo(@NotNull LiteralArgumentBuilder parent) {
+ parent.then(Commands.literal("set").requires(this::canExecute)
+ .then(Commands.argument("nickname", StringArgumentType.greedyString())
+ .suggests((ctx, builder) -> {
+ ServerPlayer player = ctx.getSource().getPlayer();
+ if (player == null) return builder.buildFuture();
+ NicknameProcessor.getInstance()
+ .getSavedNicknames(player.getUUID(), true)
+ .forEach(n -> builder.suggest(n.getNickname()));
+ return builder.buildFuture();
+ })
+ .executes(this::execute)
+ )
+ );
+ }
+
+ @Override
+ public int execute(@NotNull CommandContext ctx) throws CommandSyntaxException {
+ ServerPlayer player = ctx.getSource().getPlayer();
+ if (player == null) throw Exceptions.invalidPlayerSpecified(null);
+ String raw = StringArgumentType.getString(ctx, "nickname");
+ Nickname nickname = new Nickname(raw, NickUtils.normalizeNickname(raw));
+ if (nickname.getNormalizedNickname().isEmpty()) throw Exceptions.nickIsNull();
+ FabricSenderContext sender = new FabricSenderContext(ctx.getSource());
+ if (!NickUtils.isValidTags(sender, nickname.getNickname())) throw Exceptions.tagsNotPermitted();
+ NickUtils.nicknameChecks(sender, nickname);
+ SimpleNicksCore.get().platform().runAsync(() -> {
+ boolean success = NicknameProcessor.getInstance()
+ .setNickname(player.getUUID(), player.getGameProfile().name(), nickname.getNickname());
+ if (success) {
+ SimpleNicksCore.get().platform().runSync(() -> {
+ NickUtils.refreshDisplayName(player.getUUID());
+ sendToPlayer(player, LocaleMessage.SET_SELF, nickname);
+ });
+ } else {
+ SimpleNicksCore.get().platform().runSync(() ->
+ sendToPlayer(player, LocaleMessage.ERROR_SET_FAILURE, nickname));
+ }
+ });
+ return Command.SINGLE_SUCCESS;
+ }
+
+ @Override
+ public boolean canExecute(@NotNull CommandSourceStack source) {
+ if (source.getPlayer() == null) return false;
+ return permissionNotRequired()
+ || FabricPermissions.check(source, NickPermission.NICK_SET);
+ }
+}
diff --git a/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/FabricSubCommand.java b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/FabricSubCommand.java
new file mode 100644
index 0000000..689ab7a
--- /dev/null
+++ b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/FabricSubCommand.java
@@ -0,0 +1,110 @@
+package simplexity.simplenicks.fabric.commands.subcommands;
+
+import net.minecraft.server.players.NameAndId;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
+import net.minecraft.commands.CommandSourceStack;
+import net.minecraft.server.level.ServerPlayer;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.config.ConfigHandler;
+import simplexity.simplenicks.config.LocaleMessage;
+import simplexity.simplenicks.fabric.platform.FabricPlatformAdapter;
+import simplexity.simplenicks.logic.NickUtils;
+import simplexity.simplenicks.saving.Cache;
+import simplexity.simplenicks.saving.Nickname;
+
+import java.util.UUID;
+
+/**
+ * Interface shared by all Fabric /nick subcommands, mirroring the Paper {@code SubCommand}
+ * but typed to Fabric's {@link CommandSourceStack}.
+ */
+public interface FabricSubCommand {
+
+ void subcommandTo(@NotNull LiteralArgumentBuilder parent);
+
+ int execute(@NotNull CommandContext ctx) throws CommandSyntaxException;
+
+ boolean canExecute(@NotNull CommandSourceStack source);
+
+ default boolean permissionNotRequired() {
+ return !ConfigHandler.getInstance().isNickRequiresPermission();
+ }
+
+ default void refreshName(@NotNull UUID uuid, boolean isOnline) {
+ if (!isOnline) return;
+ SimpleNicksCore.get().platform().runSync(() -> NickUtils.refreshDisplayName(uuid));
+ }
+
+ /**
+ * Sends a MiniMessage-rendered feedback message to the command source.
+ */
+ default void sendFeedback(@NotNull CommandSourceStack source,
+ @Nullable LocaleMessage localeMessage,
+ @Nullable Nickname nickname) {
+ if (nickname == null) nickname = new Nickname("", "");
+ if (localeMessage == null || localeMessage.getMessage().isEmpty()) return;
+ Component message = SimpleNicksCore.get().miniMessage().deserialize(
+ localeMessage.getMessage(), Placeholder.parsed("value", nickname.getNickname()));
+ sendToSource(source, message);
+ }
+
+ /**
+ * Sends a MiniMessage-rendered message to a specific {@link ServerPlayer}.
+ */
+ default void sendToPlayer(@NotNull ServerPlayer player, @Nullable LocaleMessage localeMessage,
+ @Nullable Nickname nickname) {
+ if (nickname == null) nickname = new Nickname("", "");
+ if (localeMessage == null || localeMessage.getMessage().isEmpty()) return;
+ Component message = SimpleNicksCore.get().miniMessage().deserialize(
+ localeMessage.getMessage(), Placeholder.parsed("value", nickname.getNickname()));
+ FabricPlatformAdapter adapter = (FabricPlatformAdapter) SimpleNicksCore.get().platform();
+ player.sendSystemMessage(adapter.getAudiences().asNative(message));
+ }
+
+ /**
+ * Sends an Adventure component to the command source using NMS conversion.
+ */
+ default void sendToSource(@NotNull CommandSourceStack source, @NotNull Component message) {
+ FabricPlatformAdapter adapter = (FabricPlatformAdapter) SimpleNicksCore.get().platform();
+ net.minecraft.network.chat.Component nms = adapter.getAudiences().asNative(message);
+ source.sendSuccess(() -> nms, false);
+ }
+
+ /**
+ * Sends an already-built Adventure component directly to a {@link ServerPlayer}.
+ */
+ default void sendComponentToPlayer(@NotNull ServerPlayer player, @NotNull Component message) {
+ FabricPlatformAdapter adapter = (FabricPlatformAdapter) SimpleNicksCore.get().platform();
+ player.sendSystemMessage(adapter.getAudiences().asNative(message));
+ }
+
+ /**
+ * Builds the admin feedback message that includes the initiator name, target name, and value.
+ * Mirrors the Paper {@code parseAdminMessage} helper.
+ */
+ default @NotNull Component parseAdminMessage(@NotNull String template,
+ @NotNull String value,
+ @NotNull CommandSourceStack initiatorSource,
+ @NotNull NameAndId target) {
+ Component initiatorName;
+ ServerPlayer initiatorPlayer = initiatorSource.getPlayer();
+ if (initiatorPlayer != null) {
+ Nickname nick = Cache.getInstance().getActiveNickname(initiatorPlayer.getUUID());
+ String nameStr = nick != null ? nick.getNickname() : initiatorPlayer.getGameProfile().name();
+ initiatorName = SimpleNicksCore.get().miniMessage().deserialize(nameStr);
+ } else {
+ initiatorName = SimpleNicksCore.get().miniMessage()
+ .deserialize(LocaleMessage.SERVER_DISPLAY_NAME.getMessage());
+ }
+ return SimpleNicksCore.get().miniMessage().deserialize(template,
+ Placeholder.parsed("value", value),
+ Placeholder.component("initiator", initiatorName),
+ Placeholder.parsed("target", target.name()));
+ }
+}
diff --git a/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/FabricWhoSubCommand.java b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/FabricWhoSubCommand.java
new file mode 100644
index 0000000..8ec65d2
--- /dev/null
+++ b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/FabricWhoSubCommand.java
@@ -0,0 +1,89 @@
+package simplexity.simplenicks.fabric.commands.subcommands;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.arguments.StringArgumentType;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import simplexity.simplenicks.fabric.util.FabricPermissions;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
+import net.minecraft.commands.CommandSourceStack;
+import net.minecraft.commands.Commands;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.config.ConfigHandler;
+import simplexity.simplenicks.config.LocaleMessage;
+import simplexity.simplenicks.config.MessageUtils;
+import simplexity.simplenicks.logic.NickUtils;
+import simplexity.simplenicks.platform.PlayerInfo;
+import simplexity.simplenicks.saving.Nickname;
+import simplexity.simplenicks.util.NickPermission;
+
+import java.util.List;
+
+public class FabricWhoSubCommand implements FabricSubCommand {
+
+ @Override
+ public void subcommandTo(@NotNull LiteralArgumentBuilder parent) {
+ parent.then(Commands.literal("who").requires(this::canExecute)
+ .then(Commands.argument("nickname", StringArgumentType.greedyString())
+ .suggests((ctx, builder) -> {
+ SimpleNicksCore.get().platform().getOnlinePlayers().forEach(uuid -> {
+ Nickname nick = simplexity.simplenicks.saving.Cache.getInstance().getActiveNickname(uuid);
+ if (nick != null) builder.suggest(nick.getNormalizedNickname());
+ });
+ return builder.buildFuture();
+ })
+ .executes(this::execute)
+ )
+ );
+ }
+
+ @Override
+ public int execute(@NotNull CommandContext ctx) {
+ CommandSourceStack source = ctx.getSource();
+ String raw = StringArgumentType.getString(ctx, "nickname");
+ String normalized = NickUtils.normalizeNickname(raw);
+ SimpleNicksCore.get().platform().runAsync(() -> {
+ List players = NickUtils.getPlayersByNickname(normalized);
+ if (players.isEmpty()) {
+ sendFeedback(source, LocaleMessage.ERROR_NO_PLAYERS_WITH_THIS_NAME, null);
+ return;
+ }
+ Nickname nickname = new Nickname(raw, normalized);
+ sendToSource(source, buildWhoMessage(nickname, players));
+ });
+ return Command.SINGLE_SUCCESS;
+ }
+
+ @NotNull
+ private Component buildWhoMessage(@NotNull Nickname nickname, @NotNull List players) {
+ Component header = SimpleNicksCore.get().miniMessage().deserialize(
+ LocaleMessage.WHO_HEADER.getMessage(),
+ Placeholder.parsed("value", nickname.getNormalizedNickname()));
+
+ if (players.isEmpty()) {
+ return header.append(SimpleNicksCore.get().miniMessage()
+ .deserialize(LocaleMessage.INSERT_NONE.getMessage()));
+ }
+
+ Component result = header;
+ long now = System.currentTimeMillis();
+ for (PlayerInfo player : players) {
+ long timeDiffSeconds = player.lastLoginMillis() > 0
+ ? (now - player.lastLoginMillis()) / 1000
+ : 0;
+ result = result.append(SimpleNicksCore.get().miniMessage().deserialize(
+ LocaleMessage.WHO_INFO.getMessage(),
+ Placeholder.parsed("name", player.username()),
+ MessageUtils.getTimeFormat(timeDiffSeconds)));
+ }
+ return result;
+ }
+
+ @Override
+ public boolean canExecute(@NotNull CommandSourceStack source) {
+ return !ConfigHandler.getInstance().isWhoRequiresPermission()
+ || FabricPermissions.check(source, NickPermission.NICK_WHO);
+ }
+}
diff --git a/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/admin/FabricAdminDeleteSubCommand.java b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/admin/FabricAdminDeleteSubCommand.java
new file mode 100644
index 0000000..a3dc21d
--- /dev/null
+++ b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/admin/FabricAdminDeleteSubCommand.java
@@ -0,0 +1,95 @@
+package simplexity.simplenicks.fabric.commands.subcommands.admin;
+
+import net.minecraft.server.players.NameAndId;
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.arguments.StringArgumentType;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import simplexity.simplenicks.fabric.util.FabricPermissions;
+import net.minecraft.commands.CommandSourceStack;
+import net.minecraft.commands.Commands;
+import net.minecraft.commands.arguments.GameProfileArgument;
+import net.minecraft.server.level.ServerPlayer;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.commands.NicknameProcessor;
+import simplexity.simplenicks.commands.subcommands.Exceptions;
+import simplexity.simplenicks.config.LocaleMessage;
+import simplexity.simplenicks.fabric.commands.subcommands.FabricSubCommand;
+import simplexity.simplenicks.logic.NickUtils;
+import simplexity.simplenicks.saving.Nickname;
+import simplexity.simplenicks.util.NickPermission;
+
+import java.util.Collection;
+
+public class FabricAdminDeleteSubCommand implements FabricSubCommand {
+
+ @Override
+ public void subcommandTo(@NotNull LiteralArgumentBuilder parent) {
+ parent.then(Commands.literal("delete").requires(this::canExecute)
+ .then(Commands.argument("player", GameProfileArgument.gameProfile())
+ .suggests((ctx, builder) -> {
+ ctx.getSource().getServer().getPlayerList().getPlayers()
+ .forEach(p -> builder.suggest(p.getGameProfile().name()));
+ return builder.buildFuture();
+ })
+ .then(Commands.argument("nickname", StringArgumentType.greedyString())
+ .suggests((ctx, builder) -> {
+ Collection profiles;
+ try {
+ profiles = GameProfileArgument.getGameProfiles(ctx, "player");
+ } catch (CommandSyntaxException e) {
+ return builder.buildFuture();
+ }
+ profiles.stream().findFirst().ifPresent(profile -> {
+ boolean online = ctx.getSource().getServer()
+ .getPlayerList().getPlayer(profile.id()) != null;
+ NicknameProcessor.getInstance()
+ .getSavedNicknames(profile.id(), online)
+ .forEach(n -> builder.suggest(n.getNickname()));
+ });
+ return builder.buildFuture();
+ })
+ .executes(this::execute)
+ )
+ )
+ );
+ }
+
+ @Override
+ public int execute(@NotNull CommandContext ctx) throws CommandSyntaxException {
+ Collection profiles = GameProfileArgument.getGameProfiles(ctx, "player");
+ NameAndId target = profiles.stream().findFirst().orElseThrow(
+ () -> Exceptions.invalidPlayerSpecified(null));
+ String raw = StringArgumentType.getString(ctx, "nickname");
+ Nickname nickname = new Nickname(raw, NickUtils.normalizeNickname(raw));
+ CommandSourceStack source = ctx.getSource();
+ SimpleNicksCore.get().platform().runAsync(() -> {
+ boolean success = NicknameProcessor.getInstance()
+ .deleteNickname(target.id(), nickname.getNickname());
+ if (success) {
+ SimpleNicksCore.get().platform().runSync(() -> {
+ ServerPlayer onlineTarget = source.getServer().getPlayerList().getPlayer(target.id());
+ if (onlineTarget != null) {
+ NickUtils.refreshDisplayName(target.id());
+ sendComponentToPlayer(onlineTarget, parseAdminMessage(
+ LocaleMessage.DELETED_BY_INITIATOR.getMessage(),
+ nickname.getNickname(), source, target));
+ }
+ sendToSource(source, parseAdminMessage(LocaleMessage.DELETED_TARGET.getMessage(),
+ nickname.getNickname(), source, target));
+ });
+ } else {
+ SimpleNicksCore.get().platform().runSync(() ->
+ sendFeedback(source, LocaleMessage.ERROR_DELETE_FAILURE, nickname));
+ }
+ });
+ return Command.SINGLE_SUCCESS;
+ }
+
+ @Override
+ public boolean canExecute(@NotNull CommandSourceStack source) {
+ return FabricPermissions.check(source, NickPermission.NICK_ADMIN_DELETE);
+ }
+}
diff --git a/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/admin/FabricAdminLookupSubCommand.java b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/admin/FabricAdminLookupSubCommand.java
new file mode 100644
index 0000000..4d5a007
--- /dev/null
+++ b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/admin/FabricAdminLookupSubCommand.java
@@ -0,0 +1,86 @@
+package simplexity.simplenicks.fabric.commands.subcommands.admin;
+
+import net.minecraft.server.players.NameAndId;
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import simplexity.simplenicks.fabric.util.FabricPermissions;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
+import net.minecraft.commands.CommandSourceStack;
+import net.minecraft.commands.Commands;
+import net.minecraft.commands.arguments.GameProfileArgument;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.commands.NicknameProcessor;
+import simplexity.simplenicks.commands.subcommands.Exceptions;
+import simplexity.simplenicks.config.LocaleMessage;
+import simplexity.simplenicks.config.MessageUtils;
+import simplexity.simplenicks.fabric.commands.subcommands.FabricSubCommand;
+import simplexity.simplenicks.saving.Nickname;
+import simplexity.simplenicks.util.NickPermission;
+
+import java.util.Collection;
+import java.util.List;
+
+public class FabricAdminLookupSubCommand implements FabricSubCommand {
+
+ @Override
+ public void subcommandTo(@NotNull LiteralArgumentBuilder parent) {
+ parent.then(Commands.literal("lookup").requires(this::canExecute)
+ .then(Commands.argument("player", GameProfileArgument.gameProfile())
+ .suggests((ctx, builder) -> {
+ ctx.getSource().getServer().getPlayerList().getPlayers()
+ .forEach(p -> builder.suggest(p.getGameProfile().name()));
+ return builder.buildFuture();
+ })
+ .executes(this::execute)
+ )
+ );
+ }
+
+ @Override
+ public int execute(@NotNull CommandContext ctx) throws CommandSyntaxException {
+ Collection profiles = GameProfileArgument.getGameProfiles(ctx, "player");
+ NameAndId target = profiles.stream().findFirst().orElseThrow(
+ () -> Exceptions.invalidPlayerSpecified(null));
+ CommandSourceStack source = ctx.getSource();
+ boolean isOnline = source.getServer().getPlayerList().getPlayer(target.id()) != null;
+ SimpleNicksCore.get().platform().runAsync(() -> {
+ Nickname current = NicknameProcessor.getInstance().getCurrentNickname(target.id(), isOnline);
+ List saved = NicknameProcessor.getInstance().getSavedNicknames(target.id(), isOnline);
+ sendToSource(source, buildLookupMessage(target.name(), current, saved));
+ });
+ return Command.SINGLE_SUCCESS;
+ }
+
+ @NotNull
+ private Component buildLookupMessage(@NotNull String username,
+ @Nullable Nickname current,
+ @Nullable List saved) {
+ String nickname;
+ if (current == null) {
+ if (saved == null || saved.isEmpty()) {
+ return SimpleNicksCore.get().miniMessage()
+ .deserialize(LocaleMessage.ERROR_NO_PLAYERS_WITH_THIS_NAME.getMessage());
+ }
+ nickname = LocaleMessage.INSERT_NONE.getMessage();
+ } else {
+ nickname = current.getNickname();
+ }
+ String template = LocaleMessage.LOOKUP_HEADER.getMessage()
+ + LocaleMessage.LOOKUP_CURRENT.getMessage()
+ + LocaleMessage.LOOKUP_SAVED.getMessage();
+ return SimpleNicksCore.get().miniMessage().deserialize(template,
+ Placeholder.unparsed("username", username),
+ Placeholder.parsed("name", nickname),
+ MessageUtils.savedNickListResolver(saved));
+ }
+
+ @Override
+ public boolean canExecute(@NotNull CommandSourceStack source) {
+ return FabricPermissions.check(source, NickPermission.NICK_ADMIN_LOOKUP);
+ }
+}
diff --git a/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/admin/FabricAdminResetSubCommand.java b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/admin/FabricAdminResetSubCommand.java
new file mode 100644
index 0000000..2323c0b
--- /dev/null
+++ b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/admin/FabricAdminResetSubCommand.java
@@ -0,0 +1,71 @@
+package simplexity.simplenicks.fabric.commands.subcommands.admin;
+
+import net.minecraft.server.players.NameAndId;
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import simplexity.simplenicks.fabric.util.FabricPermissions;
+import net.minecraft.commands.CommandSourceStack;
+import net.minecraft.commands.Commands;
+import net.minecraft.commands.arguments.GameProfileArgument;
+import net.minecraft.server.level.ServerPlayer;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.commands.NicknameProcessor;
+import simplexity.simplenicks.commands.subcommands.Exceptions;
+import simplexity.simplenicks.config.LocaleMessage;
+import simplexity.simplenicks.fabric.commands.subcommands.FabricSubCommand;
+import simplexity.simplenicks.logic.NickUtils;
+import simplexity.simplenicks.util.NickPermission;
+
+import java.util.Collection;
+
+public class FabricAdminResetSubCommand implements FabricSubCommand {
+
+ @Override
+ public void subcommandTo(@NotNull LiteralArgumentBuilder parent) {
+ parent.then(Commands.literal("reset").requires(this::canExecute)
+ .then(Commands.argument("player", GameProfileArgument.gameProfile())
+ .suggests((ctx, builder) -> {
+ ctx.getSource().getServer().getPlayerList().getPlayers()
+ .forEach(p -> builder.suggest(p.getGameProfile().name()));
+ return builder.buildFuture();
+ })
+ .executes(this::execute)
+ )
+ );
+ }
+
+ @Override
+ public int execute(@NotNull CommandContext ctx) throws CommandSyntaxException {
+ Collection profiles = GameProfileArgument.getGameProfiles(ctx, "player");
+ NameAndId target = profiles.stream().findFirst().orElseThrow(
+ () -> Exceptions.invalidPlayerSpecified(null));
+ CommandSourceStack source = ctx.getSource();
+ SimpleNicksCore.get().platform().runAsync(() -> {
+ boolean success = NicknameProcessor.getInstance().resetNickname(target.id());
+ if (success) {
+ SimpleNicksCore.get().platform().runSync(() -> {
+ ServerPlayer onlineTarget = source.getServer().getPlayerList().getPlayer(target.id());
+ if (onlineTarget != null) {
+ NickUtils.refreshDisplayName(target.id());
+ sendComponentToPlayer(onlineTarget, parseAdminMessage(
+ LocaleMessage.RESET_BY_INITIATOR.getMessage(), "", source, target));
+ }
+ sendToSource(source, parseAdminMessage(
+ LocaleMessage.RESET_TARGET.getMessage(), "", source, target));
+ });
+ } else {
+ SimpleNicksCore.get().platform().runSync(() ->
+ sendFeedback(source, LocaleMessage.ERROR_RESET_FAILURE, null));
+ }
+ });
+ return Command.SINGLE_SUCCESS;
+ }
+
+ @Override
+ public boolean canExecute(@NotNull CommandSourceStack source) {
+ return FabricPermissions.check(source, NickPermission.NICK_ADMIN_RESET);
+ }
+}
diff --git a/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/admin/FabricAdminSetSubCommand.java b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/admin/FabricAdminSetSubCommand.java
new file mode 100644
index 0000000..a07d2cf
--- /dev/null
+++ b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/admin/FabricAdminSetSubCommand.java
@@ -0,0 +1,84 @@
+package simplexity.simplenicks.fabric.commands.subcommands.admin;
+
+import net.minecraft.server.players.NameAndId;
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.arguments.StringArgumentType;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import simplexity.simplenicks.fabric.util.FabricPermissions;
+import net.minecraft.commands.CommandSourceStack;
+import net.minecraft.commands.Commands;
+import net.minecraft.commands.arguments.GameProfileArgument;
+import net.minecraft.server.level.ServerPlayer;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.commands.NicknameProcessor;
+import simplexity.simplenicks.commands.subcommands.Exceptions;
+import simplexity.simplenicks.config.LocaleMessage;
+import simplexity.simplenicks.fabric.commands.subcommands.FabricSubCommand;
+import simplexity.simplenicks.fabric.platform.FabricSenderContext;
+import simplexity.simplenicks.logic.NickUtils;
+import simplexity.simplenicks.saving.Nickname;
+import simplexity.simplenicks.util.NickPermission;
+
+import java.util.Collection;
+
+public class FabricAdminSetSubCommand implements FabricSubCommand {
+
+ @Override
+ public void subcommandTo(@NotNull LiteralArgumentBuilder parent) {
+ parent.then(Commands.literal("set").requires(this::canExecute)
+ .then(Commands.argument("player", GameProfileArgument.gameProfile())
+ .suggests((ctx, builder) -> {
+ ctx.getSource().getServer().getPlayerList().getPlayers()
+ .forEach(p -> builder.suggest(p.getGameProfile().name()));
+ return builder.buildFuture();
+ })
+ .then(Commands.argument("nickname", StringArgumentType.greedyString())
+ .executes(this::execute)
+ )
+ )
+ );
+ }
+
+ @Override
+ public int execute(@NotNull CommandContext ctx) throws CommandSyntaxException {
+ Collection profiles = GameProfileArgument.getGameProfiles(ctx, "player");
+ NameAndId target = profiles.stream().findFirst().orElseThrow(
+ () -> Exceptions.invalidPlayerSpecified(null));
+ String raw = StringArgumentType.getString(ctx, "nickname");
+ Nickname nickname = new Nickname(raw, NickUtils.normalizeNickname(raw));
+ FabricSenderContext senderCtx = new FabricSenderContext(ctx.getSource());
+ if (!NickUtils.isValidTags(senderCtx, nickname.getNickname())) throw Exceptions.tagsNotPermitted();
+ NickUtils.nicknameChecks(senderCtx, nickname);
+ CommandSourceStack source = ctx.getSource();
+ SimpleNicksCore.get().platform().runAsync(() -> {
+ boolean success = NicknameProcessor.getInstance()
+ .setNickname(target.id(), target.name(), nickname.getNickname());
+ if (success) {
+ SimpleNicksCore.get().platform().runSync(() -> {
+ ServerPlayer onlineTarget = source.getServer().getPlayerList().getPlayer(target.id());
+ if (onlineTarget != null) {
+ NickUtils.refreshDisplayName(target.id());
+ sendComponentToPlayer(onlineTarget, parseAdminMessage(
+ LocaleMessage.SET_BY_INITIATOR.getMessage(),
+ nickname.getNickname(), source, target));
+ }
+ sendToSource(source, parseAdminMessage(LocaleMessage.SET_TARGET.getMessage(),
+ nickname.getNickname(), source, target));
+ });
+ } else {
+ SimpleNicksCore.get().platform().runSync(() ->
+ sendFeedback(source, LocaleMessage.ERROR_SET_FAILURE, nickname));
+ }
+ });
+ return Command.SINGLE_SUCCESS;
+ }
+
+ @Override
+ public boolean canExecute(@NotNull CommandSourceStack source) {
+ return FabricPermissions.check(source, NickPermission.NICK_ADMIN_SET);
+ }
+
+}
diff --git a/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/admin/FabricAdminSubCommand.java b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/admin/FabricAdminSubCommand.java
new file mode 100644
index 0000000..83d97f4
--- /dev/null
+++ b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/subcommands/admin/FabricAdminSubCommand.java
@@ -0,0 +1,34 @@
+package simplexity.simplenicks.fabric.commands.subcommands.admin;
+
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import simplexity.simplenicks.fabric.util.FabricPermissions;
+import net.minecraft.commands.CommandSourceStack;
+import net.minecraft.commands.Commands;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.fabric.commands.subcommands.FabricSubCommand;
+import simplexity.simplenicks.util.NickPermission;
+
+public class FabricAdminSubCommand implements FabricSubCommand {
+
+ @Override
+ public void subcommandTo(@NotNull LiteralArgumentBuilder parent) {
+ LiteralArgumentBuilder admin =
+ Commands.literal("admin").requires(this::canExecute);
+ new FabricAdminSetSubCommand().subcommandTo(admin);
+ new FabricAdminResetSubCommand().subcommandTo(admin);
+ new FabricAdminLookupSubCommand().subcommandTo(admin);
+ new FabricAdminDeleteSubCommand().subcommandTo(admin);
+ parent.then(admin);
+ }
+
+ @Override
+ public int execute(@NotNull CommandContext ctx) {
+ throw new UnsupportedOperationException("FabricAdminSubCommand::execute should never be called directly.");
+ }
+
+ @Override
+ public boolean canExecute(@NotNull CommandSourceStack source) {
+ return FabricPermissions.check(source, NickPermission.NICK_ADMIN);
+ }
+}
diff --git a/fabric/src/main/java/simplexity/simplenicks/fabric/events/FabricLeaveHandler.java b/fabric/src/main/java/simplexity/simplenicks/fabric/events/FabricLeaveHandler.java
new file mode 100644
index 0000000..04586e2
--- /dev/null
+++ b/fabric/src/main/java/simplexity/simplenicks/fabric/events/FabricLeaveHandler.java
@@ -0,0 +1,25 @@
+package simplexity.simplenicks.fabric.events;
+
+import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.fabric.platform.FabricPlatformAdapter;
+import simplexity.simplenicks.fabric.storage.FabricNicknameStorage;
+import simplexity.simplenicks.saving.Cache;
+
+import java.util.UUID;
+
+/**
+ * Handles player disconnect events on Fabric, mirroring {@code LeaveListener} on Paper.
+ */
+public class FabricLeaveHandler {
+
+ public static void register() {
+ ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> {
+ UUID uuid = handler.player.getUUID();
+ Cache.getInstance().removePlayerFromCache(uuid);
+ FabricNicknameStorage.clearDisplayName(uuid);
+ FabricNicknameStorage.clearTabName(uuid);
+ ((FabricPlatformAdapter) SimpleNicksCore.get().platform()).markOffline(uuid);
+ });
+ }
+}
diff --git a/fabric/src/main/java/simplexity/simplenicks/fabric/events/FabricLoginHandler.java b/fabric/src/main/java/simplexity/simplenicks/fabric/events/FabricLoginHandler.java
new file mode 100644
index 0000000..74ffc05
--- /dev/null
+++ b/fabric/src/main/java/simplexity/simplenicks/fabric/events/FabricLoginHandler.java
@@ -0,0 +1,66 @@
+package simplexity.simplenicks.fabric.events;
+
+import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.minimessage.MiniMessage;
+import net.minecraft.server.network.ServerGamePacketListenerImpl;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.config.ConfigHandler;
+import simplexity.simplenicks.fabric.platform.FabricPlatformAdapter;
+import simplexity.simplenicks.fabric.storage.FabricNicknameStorage;
+import simplexity.simplenicks.saving.Cache;
+import simplexity.simplenicks.logic.NickUtils;
+import simplexity.simplenicks.saving.Cache;
+import simplexity.simplenicks.saving.Nickname;
+import simplexity.simplenicks.saving.SqlHandler;
+
+import java.util.UUID;
+
+/**
+ * Handles player join events on Fabric, mirroring {@code LoginListener} on Paper.
+ */
+public class FabricLoginHandler {
+
+ public static void register() {
+ ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> {
+ ServerGamePacketListenerImpl listener = handler;
+ UUID playerUuid = listener.player.getUUID();
+ String username = listener.player.getGameProfile().name();
+ // Mark online immediately (main thread, before async task) so Cache.playerIsOffline()
+ // returns false when called from the virtual thread.
+ ((FabricPlatformAdapter) SimpleNicksCore.get().platform()).markOnline(playerUuid);
+ SimpleNicksCore.get().platform().runAsync(() -> {
+ SqlHandler.getInstance().updatePlayerTable(playerUuid, username);
+ Cache.getInstance().loadCurrentNickname(playerUuid);
+ Cache.getInstance().loadSavedNicknames(playerUuid);
+ // Write to FabricNicknameStorage immediately on this thread.
+ // ConcurrentHashMap writes and pure Adventure→NMS conversion are both
+ // thread-safe, so any chat message that arrives before the runSync below
+ // executes will already see the correct display name via the mixin.
+ preloadNicknameStorage(playerUuid);
+ // Full refresh on the main thread: safe player-list access + tab-list packet.
+ SimpleNicksCore.get().platform().runSync(() -> NickUtils.refreshDisplayName(playerUuid));
+ });
+ });
+ }
+
+ /**
+ * Writes the player's active nickname directly into {@link FabricNicknameStorage} without
+ * going through {@code NickUtils.refreshDisplayName}, which requires the main thread for its
+ * {@code isPlayerOnline} check. Safe to call from any thread.
+ */
+ private static void preloadNicknameStorage(@NotNull UUID playerUuid) {
+ Nickname nick = Cache.getInstance().getActiveNickname(playerUuid);
+ if (nick == null) return;
+ FabricPlatformAdapter adapter = (FabricPlatformAdapter) SimpleNicksCore.get().platform();
+ MiniMessage mm = SimpleNicksCore.get().miniMessage();
+ Component displayName = mm.deserialize(ConfigHandler.getInstance().getNickPrefix())
+ .append(mm.deserialize(nick.getNickname()));
+ FabricNicknameStorage.setDisplayName(playerUuid, adapter.getAudiences().asNative(displayName));
+ if (ConfigHandler.getInstance().shouldNickTablist()) {
+ FabricNicknameStorage.setTabName(playerUuid,
+ adapter.getAudiences().asNative(mm.deserialize(nick.getNickname())));
+ }
+ }
+}
diff --git a/fabric/src/main/java/simplexity/simplenicks/fabric/mixin/PlayerMixin.java b/fabric/src/main/java/simplexity/simplenicks/fabric/mixin/PlayerMixin.java
new file mode 100644
index 0000000..04adde3
--- /dev/null
+++ b/fabric/src/main/java/simplexity/simplenicks/fabric/mixin/PlayerMixin.java
@@ -0,0 +1,29 @@
+package simplexity.simplenicks.fabric.mixin;
+
+import net.minecraft.network.chat.Component;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.entity.player.Player;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+import simplexity.simplenicks.fabric.storage.FabricNicknameStorage;
+
+/**
+ * Intercepts {@link Player#getDisplayName()} so that SimpleNicks nicknames appear
+ * in chat-formatted messages. Only modifies the return value for {@link ServerPlayer}
+ * instances; client-side players are unaffected.
+ */
+@Mixin(Player.class)
+public abstract class PlayerMixin {
+
+ @Inject(method = "getDisplayName()Lnet/minecraft/network/chat/Component;",
+ at = @At("HEAD"), cancellable = true)
+ private void simplenicks$getDisplayName(CallbackInfoReturnable cir) {
+ if (!((Object) this instanceof ServerPlayer sp)) return;
+ Component stored = FabricNicknameStorage.getDisplayName(sp.getUUID());
+ if (stored != null) {
+ cir.setReturnValue(stored);
+ }
+ }
+}
diff --git a/fabric/src/main/java/simplexity/simplenicks/fabric/mixin/ServerLoginPacketListenerAccessor.java b/fabric/src/main/java/simplexity/simplenicks/fabric/mixin/ServerLoginPacketListenerAccessor.java
new file mode 100644
index 0000000..47bf7d0
--- /dev/null
+++ b/fabric/src/main/java/simplexity/simplenicks/fabric/mixin/ServerLoginPacketListenerAccessor.java
@@ -0,0 +1,14 @@
+package simplexity.simplenicks.fabric.mixin;
+
+import com.mojang.authlib.GameProfile;
+import net.minecraft.server.network.ServerLoginPacketListenerImpl;
+import org.jetbrains.annotations.Nullable;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+@Mixin(ServerLoginPacketListenerImpl.class)
+public interface ServerLoginPacketListenerAccessor {
+
+ @Accessor("authenticatedProfile")
+ @Nullable GameProfile getAuthenticatedProfile();
+}
diff --git a/fabric/src/main/java/simplexity/simplenicks/fabric/mixin/ServerPlayerMixin.java b/fabric/src/main/java/simplexity/simplenicks/fabric/mixin/ServerPlayerMixin.java
new file mode 100644
index 0000000..c097093
--- /dev/null
+++ b/fabric/src/main/java/simplexity/simplenicks/fabric/mixin/ServerPlayerMixin.java
@@ -0,0 +1,31 @@
+package simplexity.simplenicks.fabric.mixin;
+
+import net.minecraft.network.chat.Component;
+import net.minecraft.server.level.ServerPlayer;
+import org.jetbrains.annotations.Nullable;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+import simplexity.simplenicks.fabric.storage.FabricNicknameStorage;
+
+/**
+ * Intercepts {@link ServerPlayer#getTabListDisplayName()} so that SimpleNicks
+ * nicknames appear in the tab list without manual packet construction.
+ *
+ * Chat display name is handled in {@link PlayerMixin} since {@code getDisplayName()}
+ * is defined on {@link net.minecraft.world.entity.player.Player}, not {@link ServerPlayer}.
+ *
+ */
+@Mixin(ServerPlayer.class)
+public abstract class ServerPlayerMixin {
+
+ @Inject(method = "getTabListDisplayName()Lnet/minecraft/network/chat/Component;",
+ at = @At("HEAD"), cancellable = true)
+ private void simplenicks$getTabListDisplayName(CallbackInfoReturnable<@Nullable Component> cir) {
+ Component stored = FabricNicknameStorage.getTabName(((ServerPlayer) (Object) this).getUUID());
+ if (stored != null) {
+ cir.setReturnValue(stored);
+ }
+ }
+}
diff --git a/fabric/src/main/java/simplexity/simplenicks/fabric/platform/FabricConfigProvider.java b/fabric/src/main/java/simplexity/simplenicks/fabric/platform/FabricConfigProvider.java
new file mode 100644
index 0000000..a492a58
--- /dev/null
+++ b/fabric/src/main/java/simplexity/simplenicks/fabric/platform/FabricConfigProvider.java
@@ -0,0 +1,139 @@
+package simplexity.simplenicks.fabric.platform;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.yaml.snakeyaml.DumperOptions;
+import org.yaml.snakeyaml.Yaml;
+import simplexity.simplenicks.platform.ConfigProvider;
+
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * {@link ConfigProvider} backed by a plain YAML file via SnakeYAML.
+ * Used for both {@code config.yml} and {@code locale.yml} on Fabric.
+ */
+@SuppressWarnings("unchecked")
+public class FabricConfigProvider implements ConfigProvider {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(FabricConfigProvider.class);
+
+ private final Path file;
+ private Map data = new LinkedHashMap<>();
+ private final Yaml yaml;
+
+ public FabricConfigProvider(@NotNull Path file) {
+ this.file = file;
+ DumperOptions opts = new DumperOptions();
+ opts.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
+ opts.setPrettyFlow(true);
+ this.yaml = new Yaml(opts);
+ }
+
+ @Override
+ public void reload() {
+ try {
+ Files.createDirectories(file.getParent());
+ if (!Files.exists(file) || Files.size(file) == 0) {
+ copyDefaultResource();
+ }
+ try (FileReader reader = new FileReader(file.toFile())) {
+ Map loaded = yaml.load(reader);
+ data = loaded != null ? loaded : new LinkedHashMap<>();
+ }
+ } catch (IOException e) {
+ LOGGER.warn("Failed to load config file '{}': {}", file, e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Copies the bundled default resource matching this file's name into the config directory.
+ * Only called when the destination file is absent or empty.
+ */
+ private void copyDefaultResource() throws IOException {
+ String resourceName = file.getFileName().toString();
+ try (InputStream in = FabricConfigProvider.class.getResourceAsStream("/" + resourceName)) {
+ if (in != null) {
+ Files.copy(in, file, StandardCopyOption.REPLACE_EXISTING);
+ } else {
+ Files.createFile(file);
+ }
+ }
+ }
+
+ @Override
+ public @Nullable String getString(@NotNull String key, @Nullable String defaultValue) {
+ Object value = resolve(key);
+ if (value == null) return defaultValue;
+ return value.toString();
+ }
+
+ @Override
+ public int getInt(@NotNull String key, int defaultValue) {
+ Object value = resolve(key);
+ if (value instanceof Number n) return n.intValue();
+ return defaultValue;
+ }
+
+ @Override
+ public boolean getBoolean(@NotNull String key, boolean defaultValue) {
+ Object value = resolve(key);
+ if (value instanceof Boolean b) return b;
+ return defaultValue;
+ }
+
+ @Override
+ public long getLong(@NotNull String key, long defaultValue) {
+ Object value = resolve(key);
+ if (value instanceof Number n) return n.longValue();
+ return defaultValue;
+ }
+
+ @Override
+ public void set(@NotNull String key, @Nullable Object value) {
+ String[] parts = key.split("\\.");
+ Map current = data;
+ for (int i = 0; i < parts.length - 1; i++) {
+ current = (Map) current.computeIfAbsent(parts[i], k -> new LinkedHashMap<>());
+ }
+ if (value == null) {
+ current.remove(parts[parts.length - 1]);
+ } else {
+ current.put(parts[parts.length - 1], value);
+ }
+ }
+
+ @Override
+ public void save() {
+ try (FileWriter writer = new FileWriter(file.toFile())) {
+ yaml.dump(data, writer);
+ } catch (IOException e) {
+ LOGGER.warn("Failed to save config file '{}': {}", file, e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public boolean contains(@NotNull String key) {
+ return resolve(key) != null;
+ }
+
+ @Nullable
+ private Object resolve(@NotNull String key) {
+ String[] parts = key.split("\\.");
+ Object current = data;
+ for (String part : parts) {
+ if (!(current instanceof Map)) return null;
+ current = ((Map, ?>) current).get(part);
+ }
+ return current;
+ }
+}
diff --git a/fabric/src/main/java/simplexity/simplenicks/fabric/platform/FabricPlatformAdapter.java b/fabric/src/main/java/simplexity/simplenicks/fabric/platform/FabricPlatformAdapter.java
new file mode 100644
index 0000000..2e7ab47
--- /dev/null
+++ b/fabric/src/main/java/simplexity/simplenicks/fabric/platform/FabricPlatformAdapter.java
@@ -0,0 +1,200 @@
+package simplexity.simplenicks.fabric.platform;
+
+import net.kyori.adventure.platform.modcommon.MinecraftServerAudiences;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.minimessage.MiniMessage;
+import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
+import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerPlayer;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import simplexity.simplenicks.fabric.storage.FabricNicknameStorage;
+import simplexity.simplenicks.fabric.util.FabricPermissions;
+import simplexity.simplenicks.platform.ConfigProvider;
+import simplexity.simplenicks.platform.PlatformAdapter;
+import simplexity.simplenicks.util.ColorTag;
+import simplexity.simplenicks.util.FormatTag;
+
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+/**
+ * {@link PlatformAdapter} implementation for the Fabric platform.
+ */
+public class FabricPlatformAdapter implements PlatformAdapter {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger("SimpleNicks");
+
+ private final MinecraftServer server;
+ private final MiniMessage miniMessage;
+ private final FabricConfigProvider configProvider;
+ private final FabricConfigProvider localeProvider;
+ private final Path dataDirectory;
+ private final MinecraftServerAudiences audiences;
+ // Thread-safe mirror of online players so isPlayerOnline() is safe to call from async tasks.
+ private final Set onlinePlayerUuids = ConcurrentHashMap.newKeySet();
+
+ public FabricPlatformAdapter(@NotNull MinecraftServer server, @NotNull Path dataDirectory) {
+ this.server = server;
+ this.dataDirectory = dataDirectory;
+ this.miniMessage = buildMiniMessage();
+ this.configProvider = new FabricConfigProvider(dataDirectory.resolve("config.yml"));
+ this.localeProvider = new FabricConfigProvider(dataDirectory.resolve("locale.yml"));
+ this.audiences = MinecraftServerAudiences.of(server);
+ }
+
+ @Override
+ public void runAsync(@NotNull Runnable task) {
+ Thread.ofVirtual()
+ .uncaughtExceptionHandler((t, e) -> LOGGER.warn("Uncaught exception in async task", e))
+ .start(task);
+ }
+
+ @Override
+ public void runSync(@NotNull Runnable task) {
+ server.execute(task);
+ }
+
+ @Override
+ public boolean isPlayerOnline(@NotNull UUID uuid) {
+ return onlinePlayerUuids.contains(uuid);
+ }
+
+ /**
+ * Marks a player as online. Must be called on join before any async task reads
+ * {@link #isPlayerOnline(UUID)}.
+ */
+ public void markOnline(@NotNull UUID uuid) {
+ onlinePlayerUuids.add(uuid);
+ }
+
+ /**
+ * Marks a player as offline. Called on disconnect.
+ */
+ public void markOffline(@NotNull UUID uuid) {
+ onlinePlayerUuids.remove(uuid);
+ }
+
+ @Override
+ public @NotNull Optional getPlayerUsername(@NotNull UUID uuid) {
+ ServerPlayer player = server.getPlayerList().getPlayer(uuid);
+ if (player == null) return Optional.empty();
+ return Optional.of(player.getGameProfile().name());
+ }
+
+ @Override
+ public @NotNull Collection getOnlinePlayers() {
+ return server.getPlayerList().getPlayers().stream()
+ .map(ServerPlayer::getUUID)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public void setDisplayName(@NotNull UUID uuid, @NotNull Component displayName) {
+ FabricNicknameStorage.setDisplayName(uuid, audiences.asNative(displayName));
+ }
+
+ @Override
+ public void setTablistName(@NotNull UUID uuid, @NotNull Component tablistName) {
+ FabricNicknameStorage.setTabName(uuid, audiences.asNative(tablistName));
+ server.execute(() -> broadcastTabListUpdate(uuid));
+ }
+
+ @Override
+ public void clearDisplayName(@NotNull UUID uuid) {
+ FabricNicknameStorage.clearDisplayName(uuid);
+ }
+
+ @Override
+ public void clearTablistName(@NotNull UUID uuid) {
+ FabricNicknameStorage.clearTabName(uuid);
+ server.execute(() -> broadcastTabListUpdate(uuid));
+ }
+
+ private void broadcastTabListUpdate(@NotNull UUID uuid) {
+ ServerPlayer player = server.getPlayerList().getPlayer(uuid);
+ if (player == null) return;
+ ClientboundPlayerInfoUpdatePacket packet = new ClientboundPlayerInfoUpdatePacket(
+ EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME),
+ List.of(player)
+ );
+ server.getPlayerList().broadcastAll(packet);
+ }
+
+ @Override
+ public boolean hasPermission(@NotNull UUID uuid, @NotNull String permission) {
+ ServerPlayer player = server.getPlayerList().getPlayer(uuid);
+ if (player == null) return false;
+ return FabricPermissions.check(player, permission);
+ }
+
+ @Override
+ public @NotNull Path getDataDirectory() {
+ return dataDirectory;
+ }
+
+ @Override
+ public @NotNull Logger getLogger() {
+ return LOGGER;
+ }
+
+ @Override
+ public @NotNull MiniMessage getMiniMessage() {
+ return miniMessage;
+ }
+
+ @Override
+ public @NotNull ConfigProvider getConfigProvider() {
+ return configProvider;
+ }
+
+ @Override
+ public @NotNull ConfigProvider getLocaleProvider() {
+ return localeProvider;
+ }
+
+ /**
+ * Returns the underlying {@link MinecraftServer}. Used by Fabric-specific event handlers.
+ *
+ * @return the server instance
+ */
+ @NotNull
+ public MinecraftServer getServer() {
+ return server;
+ }
+
+ /**
+ * Returns the {@link MinecraftServerAudiences} instance used for Adventure ↔ NMS component
+ * conversion and message delivery.
+ *
+ * @return the audiences instance
+ */
+ @NotNull
+ public MinecraftServerAudiences getAudiences() {
+ return audiences;
+ }
+
+ @NotNull
+ private static MiniMessage buildMiniMessage() {
+ TagResolver.Builder tagResolver = TagResolver.builder();
+ for (ColorTag colorTag : ColorTag.values()) {
+ tagResolver.resolver(colorTag.getTagResolver());
+ }
+ for (FormatTag formatTag : FormatTag.values()) {
+ tagResolver.resolver(formatTag.getTagResolver());
+ }
+ return MiniMessage.builder()
+ .strict(false)
+ .tags(tagResolver.build())
+ .build();
+ }
+}
diff --git a/fabric/src/main/java/simplexity/simplenicks/fabric/platform/FabricSenderContext.java b/fabric/src/main/java/simplexity/simplenicks/fabric/platform/FabricSenderContext.java
new file mode 100644
index 0000000..3999b4d
--- /dev/null
+++ b/fabric/src/main/java/simplexity/simplenicks/fabric/platform/FabricSenderContext.java
@@ -0,0 +1,61 @@
+package simplexity.simplenicks.fabric.platform;
+
+import net.kyori.adventure.text.Component;
+import net.minecraft.commands.CommandSourceStack;
+import net.minecraft.server.level.ServerPlayer;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.fabric.platform.FabricPlatformAdapter;
+import simplexity.simplenicks.fabric.util.FabricPermissions;
+import simplexity.simplenicks.platform.SenderContext;
+
+import java.util.Optional;
+import java.util.UUID;
+
+/**
+ * {@link SenderContext} backed by a Minecraft {@link CommandSourceStack}.
+ * Permission checks delegate to fabric-permissions-api with an op-level fallback.
+ *
+ * Message delivery uses plain-text serialization into Minecraft's native text component.
+ * Full MiniMessage rendering can be added once a suitable Adventure bridge is available.
+ *
+ * The {@code ServerPlayerMixin} reads from these maps when the server queries
+ * {@code getDisplayName()} (chat) and {@code getTabListDisplayName()} (tab list).
+ * {@link simplexity.simplenicks.fabric.platform.FabricPlatformAdapter} writes to
+ * them after converting Adventure components to NMS components.
+ *
+ */
+public final class FabricNicknameStorage {
+
+ private static final Map displayNames = new ConcurrentHashMap<>();
+ private static final Map tabNames = new ConcurrentHashMap<>();
+
+ private FabricNicknameStorage() {
+ }
+
+ public static void setDisplayName(@NotNull UUID uuid, @NotNull Component component) {
+ displayNames.put(uuid, component);
+ }
+
+ public static void setTabName(@NotNull UUID uuid, @NotNull Component component) {
+ tabNames.put(uuid, component);
+ }
+
+ public static void clearDisplayName(@NotNull UUID uuid) {
+ displayNames.remove(uuid);
+ }
+
+ public static void clearTabName(@NotNull UUID uuid) {
+ tabNames.remove(uuid);
+ }
+
+ /**
+ * Returns the stored display name for the given player, or {@code null} if none is set.
+ * A {@code null} return means the mixin should not intercept and vanilla behaviour applies.
+ */
+ @Nullable
+ public static Component getDisplayName(@NotNull UUID uuid) {
+ return displayNames.get(uuid);
+ }
+
+ /**
+ * Returns the stored tab-list name for the given player, or {@code null} if none is set.
+ * A {@code null} return means the mixin should not intercept and vanilla behaviour applies.
+ */
+ @Nullable
+ public static Component getTabName(@NotNull UUID uuid) {
+ return tabNames.get(uuid);
+ }
+}
diff --git a/fabric/src/main/java/simplexity/simplenicks/fabric/util/FabricPermissions.java b/fabric/src/main/java/simplexity/simplenicks/fabric/util/FabricPermissions.java
new file mode 100644
index 0000000..640f176
--- /dev/null
+++ b/fabric/src/main/java/simplexity/simplenicks/fabric/util/FabricPermissions.java
@@ -0,0 +1,60 @@
+package simplexity.simplenicks.fabric.util;
+
+import me.lucko.fabric.api.permissions.v0.Permissions;
+import net.minecraft.commands.CommandSourceStack;
+import net.minecraft.server.level.ServerPlayer;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.util.NickPermission;
+
+/**
+ * Maps {@link NickPermission} defaults to the correct {@code Permissions.check} fallback.
+ *
+ *
{@code "true"} → granted to all players by default
+ *
{@code "op"} → granted to op-level-2+ by default
+ *
{@code "false"} → not granted to anyone by default
+ * Use {@link #forMainConfig(JavaPlugin)} for {@code config.yml} (delegates to
+ * {@link JavaPlugin#getConfig()}) and {@link #forFile(File)} for other YAML
+ * files such as {@code locale.yml}.
+ *
+ */
+@SuppressWarnings("CallToPrintStackTrace")
+public class BukkitConfigProvider implements ConfigProvider {
+
+ private final JavaPlugin plugin;
+ private final File file;
+ private YamlConfiguration yamlConfig;
+
+ private BukkitConfigProvider(JavaPlugin plugin, File file) {
+ this.plugin = plugin;
+ this.file = file;
+ }
+
+ /**
+ * Creates a provider backed by the plugin's main {@code config.yml}.
+ *
+ * @param plugin the plugin instance
+ * @return a config provider for the main config
+ */
+ @NotNull
+ public static BukkitConfigProvider forMainConfig(@NotNull JavaPlugin plugin) {
+ return new BukkitConfigProvider(plugin, null);
+ }
+
+ /**
+ * Creates a provider backed by the given YAML file. The file is created on
+ * first {@link #reload()} if it does not exist.
+ *
+ * @param file the YAML file to read/write
+ * @return a config provider for the given file
+ */
+ @NotNull
+ public static BukkitConfigProvider forFile(@NotNull File file) {
+ return new BukkitConfigProvider(null, file);
+ }
+
+ @Override
+ public void reload() {
+ if (isMainConfig()) {
+ plugin.saveDefaultConfig();
+ plugin.reloadConfig();
+ } else {
+ try {
+ if (file.getParentFile() != null) file.getParentFile().mkdirs();
+ file.createNewFile();
+ yamlConfig = new YamlConfiguration();
+ yamlConfig.load(file);
+ } catch (IOException | InvalidConfigurationException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Override
+ public @Nullable String getString(@NotNull String key, @Nullable String defaultValue) {
+ return config().getString(key, defaultValue);
+ }
+
+ @Override
+ public int getInt(@NotNull String key, int defaultValue) {
+ return config().getInt(key, defaultValue);
+ }
+
+ @Override
+ public boolean getBoolean(@NotNull String key, boolean defaultValue) {
+ return config().getBoolean(key, defaultValue);
+ }
+
+ @Override
+ public long getLong(@NotNull String key, long defaultValue) {
+ return config().getLong(key, defaultValue);
+ }
+
+ @Override
+ public void set(@NotNull String key, @Nullable Object value) {
+ config().set(key, value);
+ }
+
+ @Override
+ public void save() {
+ if (isMainConfig()) {
+ plugin.saveConfig();
+ } else {
+ try {
+ yamlConfig.save(file);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Override
+ public boolean contains(@NotNull String key) {
+ return config().contains(key);
+ }
+
+ private boolean isMainConfig() {
+ return plugin != null;
+ }
+
+ @NotNull
+ private FileConfiguration config() {
+ if (isMainConfig()) return plugin.getConfig();
+ return yamlConfig;
+ }
+}
diff --git a/paper/src/main/java/simplexity/simplenicks/platform/PaperPlatformAdapter.java b/paper/src/main/java/simplexity/simplenicks/platform/PaperPlatformAdapter.java
new file mode 100644
index 0000000..e1d1eb8
--- /dev/null
+++ b/paper/src/main/java/simplexity/simplenicks/platform/PaperPlatformAdapter.java
@@ -0,0 +1,153 @@
+package simplexity.simplenicks.platform;
+
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.minimessage.MiniMessage;
+import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+import simplexity.simplenicks.util.ColorTag;
+import simplexity.simplenicks.util.FormatTag;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+/**
+ * {@link PlatformAdapter} implementation for the Paper platform.
+ */
+public class PaperPlatformAdapter implements PlatformAdapter {
+
+ private final JavaPlugin plugin;
+ private final MiniMessage miniMessage;
+ private final BukkitConfigProvider configProvider;
+ private final BukkitConfigProvider localeProvider;
+
+ public PaperPlatformAdapter(@NotNull JavaPlugin plugin) {
+ this.plugin = plugin;
+ this.miniMessage = buildMiniMessage();
+ this.configProvider = BukkitConfigProvider.forMainConfig(plugin);
+ this.localeProvider = BukkitConfigProvider.forFile(
+ new File(plugin.getDataFolder(), "locale.yml")
+ );
+ }
+
+ @Override
+ public void runAsync(@NotNull Runnable task) {
+ Bukkit.getScheduler().runTaskAsynchronously(plugin, task);
+ }
+
+ @Override
+ public void runSync(@NotNull Runnable task) {
+ Bukkit.getScheduler().runTask(plugin, task);
+ }
+
+ @Override
+ public boolean isPlayerOnline(@NotNull UUID uuid) {
+ return Bukkit.getPlayer(uuid) != null;
+ }
+
+ @Override
+ public @NotNull Optional getPlayerUsername(@NotNull UUID uuid) {
+ return Optional.ofNullable(Bukkit.getPlayer(uuid)).map(Player::getName);
+ }
+
+ @Override
+ public @NotNull Collection getOnlinePlayers() {
+ return Bukkit.getOnlinePlayers().stream()
+ .map(Player::getUniqueId)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public void setDisplayName(@NotNull UUID uuid, @NotNull Component displayName) {
+ Player player = Bukkit.getPlayer(uuid);
+ if (player == null) return;
+ player.displayName(displayName);
+ }
+
+ @Override
+ public void setTablistName(@NotNull UUID uuid, @NotNull Component tablistName) {
+ Player player = Bukkit.getPlayer(uuid);
+ if (player == null) return;
+ player.playerListName(tablistName);
+ }
+
+ @Override
+ public void clearDisplayName(@NotNull UUID uuid) {
+ Player player = Bukkit.getPlayer(uuid);
+ if (player == null) return;
+ player.displayName(null);
+ }
+
+ @Override
+ public void clearTablistName(@NotNull UUID uuid) {
+ Player player = Bukkit.getPlayer(uuid);
+ if (player == null) return;
+ player.playerListName(null);
+ }
+
+ @Override
+ public boolean hasPermission(@NotNull UUID uuid, @NotNull String permission) {
+ Player player = Bukkit.getPlayer(uuid);
+ if (player == null) return false;
+ return player.hasPermission(permission);
+ }
+
+ @Override
+ public @NotNull Path getDataDirectory() {
+ return plugin.getDataFolder().toPath();
+ }
+
+ @Override
+ public @NotNull Logger getLogger() {
+ return plugin.getSLF4JLogger();
+ }
+
+ @Override
+ public @NotNull MiniMessage getMiniMessage() {
+ return miniMessage;
+ }
+
+ @Override
+ public @NotNull ConfigProvider getConfigProvider() {
+ return configProvider;
+ }
+
+ @Override
+ public @NotNull ConfigProvider getLocaleProvider() {
+ return localeProvider;
+ }
+
+ /**
+ * Returns the underlying Paper plugin instance. Used by Paper-specific classes
+ * such as {@link simplexity.simplenicks.saving.SaveMigrator} that need direct
+ * plugin API access.
+ *
+ * @return the plugin instance
+ */
+ @NotNull
+ public JavaPlugin getPlugin() {
+ return plugin;
+ }
+
+ @NotNull
+ private static MiniMessage buildMiniMessage() {
+ TagResolver.Builder tagResolver = TagResolver.builder();
+ for (ColorTag colorTag : ColorTag.values()) {
+ tagResolver.resolver(colorTag.getTagResolver());
+ }
+ for (FormatTag formatTag : FormatTag.values()) {
+ tagResolver.resolver(formatTag.getTagResolver());
+ }
+ return MiniMessage.builder()
+ .strict(false)
+ .tags(tagResolver.build())
+ .build();
+ }
+}
diff --git a/paper/src/main/java/simplexity/simplenicks/platform/PaperSenderContext.java b/paper/src/main/java/simplexity/simplenicks/platform/PaperSenderContext.java
new file mode 100644
index 0000000..9ceee3c
--- /dev/null
+++ b/paper/src/main/java/simplexity/simplenicks/platform/PaperSenderContext.java
@@ -0,0 +1,51 @@
+package simplexity.simplenicks.platform;
+
+import net.kyori.adventure.text.Component;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.SimpleNicksCore;
+
+import java.util.Optional;
+import java.util.UUID;
+
+/**
+ * {@link SenderContext} backed by a Bukkit {@link CommandSender}.
+ */
+public class PaperSenderContext implements SenderContext {
+
+ private final CommandSender sender;
+
+ public PaperSenderContext(@NotNull CommandSender sender) {
+ this.sender = sender;
+ }
+
+ @Override
+ public boolean hasPermission(@NotNull String permission) {
+ return sender.hasPermission(permission);
+ }
+
+ @Override
+ public @NotNull Optional getUuid() {
+ if (sender instanceof Player player) return Optional.of(player.getUniqueId());
+ return Optional.empty();
+ }
+
+ @Override
+ public void sendMessage(@NotNull Component message) {
+ sender.sendMessage(message);
+ }
+
+ @Override
+ public boolean isPlayer() {
+ return sender instanceof Player;
+ }
+
+ @Override
+ public @NotNull String getDisplayName() {
+ if (sender instanceof Player player) {
+ return SimpleNicksCore.get().miniMessage().serialize(player.displayName());
+ }
+ return "[Console]";
+ }
+}
diff --git a/src/main/java/simplexity/simplenicks/saving/SaveMigrator.java b/paper/src/main/java/simplexity/simplenicks/saving/SaveMigrator.java
similarity index 53%
rename from src/main/java/simplexity/simplenicks/saving/SaveMigrator.java
rename to paper/src/main/java/simplexity/simplenicks/saving/SaveMigrator.java
index 2bd3f95..6949f6b 100644
--- a/src/main/java/simplexity/simplenicks/saving/SaveMigrator.java
+++ b/paper/src/main/java/simplexity/simplenicks/saving/SaveMigrator.java
@@ -10,11 +10,13 @@
import org.bukkit.entity.Player;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
+import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
-import simplexity.simplenicks.SimpleNicks;
+import simplexity.simplenicks.SimpleNicksCore;
import simplexity.simplenicks.config.ConfigHandler;
import simplexity.simplenicks.logic.NickUtils;
+import simplexity.simplenicks.platform.PaperPlatformAdapter;
import java.io.File;
import java.io.IOException;
@@ -26,76 +28,59 @@
/**
* Utility class for migrating nickname save data from older storage formats into the current system.
- *
- * This class is intended to be used only once, during the upgrade of the SimpleNicks plugin.
- * It reads legacy data files or player PersistentDataContainers (PDC) and converts them into the
- * current {@link Cache} and {@link SqlHandler} format. After migration, it renames the old data file
- * to prevent duplicate migrations, while saving the old data if the migration didn't go as planned.
- *
- *
- * While designed for internal use, this class can be adapted by other plugins to import custom
- * nickname data, or for other types of one-time migrations. It should be called during plugin startup,
- * and only if there is not already saved data from these users, to prevent overwriting current data.
- *
*/
public class SaveMigrator {
- /**
- * Namespaced key used for storing nicknames in a player's PersistentDataContainer (PDC).
- * There was no implementation of 'saved' nicknames with PDC, so only the current nickname is migrated.
- */
- public static final NamespacedKey nickNameSave = new NamespacedKey(SimpleNicks.getInstance(), "nickname");
+ public static final NamespacedKey nickNameSave = new NamespacedKey(plugin(), "nickname");
- private static final Logger logger = SimpleNicks.getInstance().getSLF4JLogger();
private static final List records = new ArrayList<>();
private static final AtomicInteger processed = new AtomicInteger();
private static final AtomicInteger failed = new AtomicInteger();
private static int taskId;
+ private static Logger logger() {
+ return SimpleNicksCore.get().platform().getLogger();
+ }
+
+ private static JavaPlugin plugin() {
+ return ((PaperPlatformAdapter) SimpleNicksCore.get().platform()).getPlugin();
+ }
+
/**
* Migrates all nickname data from the legacy YAML file ("nickname_data.yml") into the current database format.
- *
- * This method performs the migration asynchronously to avoid blocking the server. It provides
- * periodic console logs with progress updates. After migration, the legacy YAML file is renamed
- * to "MIGRATED_nickname_data.yml" to prevent repeated attempts.
- *
- *
- * If any UUID keys are invalid or cannot be processed, the migration continues with a warning.
- * The method logs the number of successfully migrated and failed records.
- *
*/
public static void migrateFromYml() {
- File dataFile = new File(SimpleNicks.getInstance().getDataFolder(), "nickname_data.yml");
+ File dataFile = SimpleNicksCore.get().platform().getDataDirectory().resolve("nickname_data.yml").toFile();
if (!dataFile.exists()) return;
FileConfiguration nicknameData = new YamlConfiguration();
try {
nicknameData.load(dataFile);
} catch (IOException | InvalidConfigurationException e) {
- logger.warn("Unable to migrate nicknames from YML: {}", e.getMessage(), e);
+ logger().warn("Unable to migrate nicknames from YML: {}", e.getMessage(), e);
return;
}
- logger.info("Starting Save Migration");
+ logger().info("Starting Save Migration");
Set savedUuids = nicknameData.getKeys(false);
int totalUuids = savedUuids.size();
- taskId = Bukkit.getScheduler().scheduleSyncRepeatingTask(SimpleNicks.getInstance(), () -> consoleNotifier(totalUuids), 0L, 100L);
- Bukkit.getScheduler().runTaskAsynchronously(SimpleNicks.getInstance(), () -> {
+ taskId = Bukkit.getScheduler().scheduleSyncRepeatingTask(plugin(), () -> consoleNotifier(totalUuids), 0L, 100L);
+ SimpleNicksCore.get().platform().runAsync(() -> {
for (String uuidKey : savedUuids) {
saveChecks(uuidKey, nicknameData);
}
boolean success = SqlHandler.getInstance().batchInsertNicknames(records);
if (success) {
- logger.info("Save data migrated successfully! {} users' data migrated", processed.get());
+ logger().info("Save data migrated successfully! {} users' data migrated", processed.get());
} else {
- logger.error("Save data was not migrated properly! Please report this to the developers at https://github.com/Simplexity-Development/SimpleNicks!\nPlease be sure to attach any error logs that were created, and your config when you make a report!");
+ logger().error("Save data was not migrated properly! Please report this to the developers.");
}
- if (failed.get() > 0) logger.warn("{} users' data was not successfully migrated", failed.get());
- File backupFile = new File(SimpleNicks.getInstance().getDataFolder(), "MIGRATED_nickname_data.yml");
+ if (failed.get() > 0) logger().warn("{} users' data was not successfully migrated", failed.get());
+ File backupFile = SimpleNicksCore.get().platform().getDataDirectory().resolve("MIGRATED_nickname_data.yml").toFile();
boolean renamed = dataFile.renameTo(backupFile);
if (!renamed) {
- logger.warn("Unable to rename 'nickname_data.yml' - if migration was successful, please remove or rename this file so as to not overwrite new save data.");
+ logger().warn("Unable to rename 'nickname_data.yml' - if migration was successful, please remove or rename this file.");
} else {
- logger.info("Successfully renamed 'nickname_data.yml' - this migration process will no longer be attempted (unless you rename it back, for some reason, I wouldn't recommend that)");
+ logger().info("Successfully renamed 'nickname_data.yml' - this migration process will no longer be attempted.");
}
Bukkit.getScheduler().cancelTask(taskId);
});
@@ -103,12 +88,8 @@ public static void migrateFromYml() {
/**
* Migrates the nickname stored in a player's PersistentDataContainer (PDC) into the current {@link Cache}.
- *
- * After migration, the PDC entry is removed to prevent duplicate storage. This is safe to call
- * on individual players, typically during player login events.
- *
*
- * @param player The player whose PDC nickname should be migrated
+ * @param player the player whose PDC nickname should be migrated
*/
public static void migratePdcNickname(@NotNull Player player) {
PersistentDataContainer pdc = player.getPersistentDataContainer();
@@ -124,14 +105,6 @@ public static void migratePdcNickname(@NotNull Player player) {
pdc.remove(nickNameSave);
}
-
- /**
- * Performs sanity checks on a UUID key from the YAML configuration and adds it to the migration
- * batch if valid. Processes both the player's current nickname and saved nicknames.
- *
- * @param uuidKey UUID string of the player
- * @param config YAML configuration containing the legacy nickname data
- */
private static void saveChecks(@NotNull String uuidKey, @NotNull FileConfiguration config) {
UUID uuid;
try {
@@ -170,35 +143,16 @@ private static void saveChecks(@NotNull String uuidKey, @NotNull FileConfigurati
processed.incrementAndGet();
}
- /**
- * Periodically logs the migration progress to the console.
- *
- * Shows the percentage complete and reminds server admins not to restart the server during migration.
- *
- *
- * @param total Total number of UUIDs being migrated
- */
private static void consoleNotifier(int total) {
int done = processed.get();
double percent = (done / (double) total) * 100.0;
- logger.info("[MIGRATION] {}% complete ({} / {})", String.format("%.1f", percent), done, total);
- logger.info("[MIGRATION] ⚠ Do NOT restart the server until migration completes!");
+ logger().info("[MIGRATION] {}% complete ({} / {})", String.format("%.1f", percent), done, total);
+ logger().info("[MIGRATION] ⚠ Do NOT restart the server until migration completes!");
}
-
- /**
- * Debug logging method that only logs messages if {@link ConfigHandler#isDebugMode()} is enabled.
- *
- * Accepts printf-style formatting with arguments.
- *
- *
- * @param message The message format string
- * @param args Arguments for the format string
- */
private static void debug(@NotNull String message, @NotNull Object... args) {
if (ConfigHandler.getInstance().isDebugMode()) {
- message = String.format(message, args);
- logger.info("[MIGRATION DEBUG] {}", message);
+ logger().info("[MIGRATION DEBUG] " + String.format(message, args));
}
}
}
diff --git a/paper/src/main/resources/config.yml b/paper/src/main/resources/config.yml
new file mode 100644
index 0000000..6c9482a
--- /dev/null
+++ b/paper/src/main/resources/config.yml
@@ -0,0 +1,47 @@
+mysql:
+ enabled: false
+ ip: "localhost:3306"
+ name: simplenicks
+ username: username1
+ password: badpassword!
+
+# The max amount of characters a nickname should be, not including formatting
+# (so a name like BillyBob would only count 'BillyBob' - and would be 8 characters)
+# Setting this number to any number below "3" could cause unintended side effects
+max-nickname-length: 30
+
+# The regex of valid final nickname characters.
+# Be warned that putting non-alphanumeric characters may result in issues with other plugins and other unintended side effects
+nickname-regex: "[A-Za-z0-9_]+"
+
+# What should require permission set by a permission plugin?
+require-permission:
+ nick: true
+ color: true
+ format: true
+ who: false
+
+# Blocks certain names from being used as nicknames
+# Expiration times are in days, setting to -1 will make it so it never expires
+nickname-protection:
+ username:
+ enabled: true
+ expires: 30
+ online:
+ enabled: false
+ offline:
+ enabled: false
+ expires: 30
+
+# How many nicknames can be saved?
+max-saves: 5
+
+# Should names be changed in tablist?
+# (Keep this false if you use any other tablist plugin, there are placeholder API placeholders to use on those)
+tablist-nick: false
+
+# What prefix should be given for players who have a nickname? put "" if you want no prefix
+nickname-prefix: ""
+
+# Development option, this will flood your logs, discouraged from enabling unless asked
+debug-mode: false
diff --git a/src/main/resources/paper-plugin.yml b/paper/src/main/resources/paper-plugin.yml
similarity index 100%
rename from src/main/resources/paper-plugin.yml
rename to paper/src/main/resources/paper-plugin.yml
diff --git a/pom.xml b/pom.xml
deleted file mode 100644
index aa43fb2..0000000
--- a/pom.xml
+++ /dev/null
@@ -1,103 +0,0 @@
-
-
- 4.0.0
-
- simplexity
- SimpleNicks
- 3.2.2
- jar
-
- SimpleNicks
-
-
- 21
- UTF-8
-
-
-
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
- 3.8.1
-
- 21
- 21
-
-
-
- org.apache.maven.plugins
- maven-shade-plugin
- 3.2.4
-
-
- package
-
- shade
-
-
- false
-
-
-
- mojang
-
-
-
-
-
-
-
-
-
-
- src/main/resources
- true
-
-
-
-
-
-
- papermc-repo
- https://repo.papermc.io/repository/maven-public/
-
-
- sonatype
- https://oss.sonatype.org/content/groups/public/
-
-
- placeholderapi
- https://repo.extendedclip.com/content/repositories/placeholderapi/
-
-
-
-
-
- io.papermc.paper
- paper-api
- 1.21.5-R0.1-SNAPSHOT
- provided
-
-
- me.clip
- placeholderapi
- 2.11.5
- provided
-
-
- com.zaxxer
- HikariCP
- 6.3.0
-
-
- io.github.miniplaceholders
- miniplaceholders-api
- 3.2.0
- provided
-
-
-
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..5f3ca94
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,9 @@
+pluginManagement {
+ repositories {
+ maven { url 'https://maven.fabricmc.net/' }
+ gradlePluginPortal()
+ }
+}
+
+rootProject.name = 'SimpleNicks'
+include 'core', 'paper', 'fabric'
diff --git a/src/main/java/simplexity/simplenicks/commands/NicknameProcessor.java b/src/main/java/simplexity/simplenicks/commands/NicknameProcessor.java
deleted file mode 100644
index 97bc139..0000000
--- a/src/main/java/simplexity/simplenicks/commands/NicknameProcessor.java
+++ /dev/null
@@ -1,161 +0,0 @@
-package simplexity.simplenicks.commands;
-
-import org.bukkit.OfflinePlayer;
-import org.jetbrains.annotations.NotNull;
-import simplexity.simplenicks.saving.Cache;
-import simplexity.simplenicks.saving.Nickname;
-import simplexity.simplenicks.saving.SqlHandler;
-
-import javax.annotation.Nullable;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.UUID;
-
-/**
- * Handles the high-level logic for nickname management.
- *
- * This class acts as the main entry point for commands or other
- * external systems that want to interact with nicknames.
- * It delegates persistence and caching to {@link Cache} and {@link SqlHandler}.
- *
- */
-@SuppressWarnings("UnusedReturnValue")
-public class NicknameProcessor {
- private static NicknameProcessor instance;
-
- private NicknameProcessor() {
- }
-
- public static NicknameProcessor getInstance() {
- if (instance == null) instance = new NicknameProcessor();
- return instance;
- }
-
- /**
- * Sets a player's active nickname.
- *
- * @param player The player whose nickname is being set.
- * @param nickname The nickname string to assign.
- * @return {@code true} if the nickname was set successfully,
- * {@code false} if it failed to persist or cache.
- */
- public boolean setNickname(@NotNull OfflinePlayer player, @NotNull String nickname) {
- UUID playerUuid = player.getUniqueId();
- String username = player.getName();
- if (username == null) return false;
- return Cache.getInstance().setActiveNickname(playerUuid, username, nickname);
- }
-
- /**
- * Resets a player's nickname back to their original username.
- *
- * @param player The player whose nickname should be reset.
- * @return {@code true} if the nickname was cleared successfully,
- * {@code false} if the database update failed.
- */
- public boolean resetNickname(@NotNull OfflinePlayer player) {
- UUID playerUuid = player.getUniqueId();
- return Cache.getInstance().clearCurrentNickname(playerUuid);
- }
-
- /**
- * Saves a nickname to the player's list of saved nicknames.
- *
- * @param player The player saving the nickname.
- * @param nickname The nickname string to save.
- * @return {@code true} if the nickname was saved successfully,
- * {@code false} if it failed to persist or cache.
- */
- public boolean saveNickname(@NotNull OfflinePlayer player, @NotNull String nickname) {
- UUID playerUuid = player.getUniqueId();
- String username = player.getName();
- if (username == null) return false;
- return Cache.getInstance().saveNickname(playerUuid, username, nickname);
- }
-
- /**
- * Deletes a previously saved nickname for a player.
- *
- * @param player The player who owns the nickname.
- * @param nickname The nickname string to delete.
- * @return {@code true} if the nickname was deleted successfully,
- * {@code false} if no such nickname was found or persistence failed.
- */
- public boolean deleteNickname(@NotNull OfflinePlayer player, @NotNull String nickname) {
- UUID playerUuid = player.getUniqueId();
- return Cache.getInstance().deleteSavedNickname(playerUuid, nickname);
- }
-
-
- /**
- * Gets all saved nicknames for a player.
- *
- * Uses the in-memory cache if the player is online,
- * otherwise queries SQL directly.
- *
- *
- * @param player The player whose saved nicknames to retrieve.
- * @return A non-null list of {@link Nickname}. Empty if none exist.
- */
- @NotNull
- public List getSavedNicknames(@NotNull OfflinePlayer player) {
- UUID playerUuid = player.getUniqueId();
- boolean online = player.isOnline();
- if (online) return Cache.getInstance().getSavedNicknames(playerUuid);
- List nicks = SqlHandler.getInstance().getSavedNicknamesForPlayer(playerUuid);
- if (nicks == null) return new ArrayList<>();
- return nicks;
- }
-
- /**
- * Gets the currently active nickname for a player.
- *
- * Uses the in-memory cache if the player is online,
- * otherwise queries SQL directly.
- *
- *
- * @param player The player whose nickname to get.
- * @return The current {@link Nickname}, or {@code null} if none is set.
- */
- @Nullable
- public Nickname getCurrentNickname(@NotNull OfflinePlayer player) {
- UUID playerUuid = player.getUniqueId();
- boolean online = player.isOnline();
- if (online) return Cache.getInstance().getActiveNickname(playerUuid);
- return SqlHandler.getInstance().getCurrentNicknameForPlayer(playerUuid);
- }
-
- /**
- * Gets the number of saved nicknames for a player.
- *
- * Uses the in-memory cache if the player is online,
- * otherwise queries SQL directly.
- *
- *
- * @param player The player whose saved nicknames to count.
- * @return The number of saved nicknames.
- */
- public int getCurrentSavedNickCount(@NotNull OfflinePlayer player) {
- UUID playerUuid = player.getUniqueId();
- boolean online = player.isOnline();
- if (online) return Cache.getInstance().getSavedNickCount(playerUuid);
- List savedNicks = SqlHandler.getInstance().getSavedNicknamesForPlayer(playerUuid);
- if (savedNicks == null || savedNicks.isEmpty()) return 0;
- return savedNicks.size();
- }
-
-
- /**
- * Checks if a player has already saved the given nickname.
- *
- * @param player The player to check.
- * @param nickname The nickname string to search for.
- * @return {@code true} if the player already saved this nickname,
- * {@code false} otherwise.
- */
- public boolean playerAlreadySavedThis(@NotNull OfflinePlayer player, @NotNull String nickname) {
- UUID playerUuid = player.getUniqueId();
- return SqlHandler.getInstance().userAlreadySavedThisName(playerUuid, nickname);
- }
-
-}
diff --git a/src/main/java/simplexity/simplenicks/commands/subcommands/Exceptions.java b/src/main/java/simplexity/simplenicks/commands/subcommands/Exceptions.java
deleted file mode 100644
index 8c03569..0000000
--- a/src/main/java/simplexity/simplenicks/commands/subcommands/Exceptions.java
+++ /dev/null
@@ -1,115 +0,0 @@
-package simplexity.simplenicks.commands.subcommands;
-
-import com.mojang.brigadier.exceptions.DynamicCommandExceptionType;
-import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
-import io.papermc.paper.command.brigadier.MessageComponentSerializer;
-import net.kyori.adventure.text.minimessage.MiniMessage;
-import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
-import simplexity.simplenicks.SimpleNicks;
-import simplexity.simplenicks.config.ConfigHandler;
-import simplexity.simplenicks.config.LocaleMessage;
-
-@SuppressWarnings("UnstableApiUsage")
-public class Exceptions {
-
- private static final MiniMessage miniMessage = SimpleNicks.getMiniMessage();
-
- public static final SimpleCommandExceptionType ERROR_NICK_IS_NULL = new SimpleCommandExceptionType(
- MessageComponentSerializer.message().serialize(
- miniMessage.deserialize(
- LocaleMessage.ERROR_NICK_IS_NULL.getMessage()
- )
- )
- );
-
- public static final SimpleCommandExceptionType ERROR_EMPTY_NICK_AFTER_PARSE = new SimpleCommandExceptionType(
- MessageComponentSerializer.message().serialize(
- miniMessage.deserialize(
- LocaleMessage.ERROR_INVALID_NICK_EMPTY.getMessage()
- )
- )
- );
-
- public static final SimpleCommandExceptionType ERROR_CANNOT_SAVE = new SimpleCommandExceptionType(
- MessageComponentSerializer.message().serialize(
- miniMessage.deserialize(
- LocaleMessage.ERROR_SAVE_FAILURE.getMessage()
- )
- )
- );
-
-
- public static final SimpleCommandExceptionType ERROR_TOO_MANY_SAVED_NAMES = new SimpleCommandExceptionType(
- MessageComponentSerializer.message().serialize(
- miniMessage.deserialize(
- LocaleMessage.ERROR_TOO_MANY_TO_SAVE.getMessage()
- )
- )
- );
-
- public static final DynamicCommandExceptionType ERROR_LENGTH = new DynamicCommandExceptionType(
- nickname -> MessageComponentSerializer.message().serialize(
- miniMessage.deserialize(
- LocaleMessage.ERROR_INVALID_NICK_LENGTH.getMessage(),
- Placeholder.unparsed("value", String.valueOf(ConfigHandler.getInstance().getMaxLength())),
- Placeholder.unparsed("name", nickname.toString())
- )
- )
- );
-
- public static final DynamicCommandExceptionType ERROR_REGEX = new DynamicCommandExceptionType(
- nickname -> MessageComponentSerializer.message().serialize(
- miniMessage.deserialize(
- LocaleMessage.ERROR_INVALID_NICK.getMessage(),
- Placeholder.unparsed("regex", ConfigHandler.getInstance().getRegexString())
- )
- )
- );
-
- public static final DynamicCommandExceptionType INVALID_PLAYER_SPECIFIED = new DynamicCommandExceptionType(
- playerName -> MessageComponentSerializer.message().serialize(
- miniMessage.deserialize(
- LocaleMessage.ERROR_INVALID_PLAYER.getMessage(),
- Placeholder.unparsed("player_name", playerName.toString())
- )
- )
- );
-
- public static final DynamicCommandExceptionType ERROR_NICKNAME_IS_SOMEONES_USERNAME = new DynamicCommandExceptionType(
- nickname -> MessageComponentSerializer.message().serialize(
- miniMessage.deserialize(
- LocaleMessage.ERROR_INVALID_OTHER_PLAYERS_USERNAME.getMessage(),
- Placeholder.unparsed("value", nickname.toString())
- )
- )
- );
-
-
- public static final DynamicCommandExceptionType ERROR_SOMEONE_USING_THAT_NICKNAME = new DynamicCommandExceptionType(
- nickname -> MessageComponentSerializer.message().serialize(
- miniMessage.deserialize(
- LocaleMessage.ERROR_INVALID_OTHER_PLAYERS_NICKNAME.getMessage(),
- Placeholder.unparsed("value", nickname.toString())
- )
- )
- );
-
- public static final SimpleCommandExceptionType ERROR_TAGS_NOT_PERMITTED = new SimpleCommandExceptionType(
- MessageComponentSerializer.message().serialize(
- miniMessage.deserialize(
- LocaleMessage.ERROR_INVALID_TAGS.getMessage()
- )
- )
- );
-
- public static final SimpleCommandExceptionType ERROR_ALREADY_SAVED = new SimpleCommandExceptionType(
- MessageComponentSerializer.message().serialize(
- miniMessage.deserialize(
- LocaleMessage.ERROR_ALREADY_SAVED.getMessage()
- )
- )
- );
-
-
-
-}
diff --git a/src/main/java/simplexity/simplenicks/config/LocaleHandler.java b/src/main/java/simplexity/simplenicks/config/LocaleHandler.java
deleted file mode 100644
index 8e4be4a..0000000
--- a/src/main/java/simplexity/simplenicks/config/LocaleHandler.java
+++ /dev/null
@@ -1,88 +0,0 @@
-package simplexity.simplenicks.config;
-
-import org.bukkit.configuration.InvalidConfigurationException;
-import org.bukkit.configuration.file.FileConfiguration;
-import org.bukkit.configuration.file.YamlConfiguration;
-import simplexity.simplenicks.SimpleNicks;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-@SuppressWarnings({"CallToPrintStackTrace", "CollectionAddAllCanBeReplacedWithConstructor", "ResultOfMethodCallIgnored"})
-public class LocaleHandler {
- private static LocaleHandler instance;
- private final String fileName = "locale.yml";
- private final File dataFile = new File(SimpleNicks.getInstance().getDataFolder(), fileName);
- private FileConfiguration locale = new YamlConfiguration();
-
- private LocaleHandler() {
- try {
- dataFile.getParentFile().mkdirs();
- dataFile.createNewFile();
- reloadLocale();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
-
- public static LocaleHandler getInstance() {
- if (instance == null) {
- instance = new LocaleHandler();
- }
- return instance;
- }
-
- public void reloadLocale() {
- try {
- locale.load(dataFile);
- populateLocale();
- sortLocale();
- saveLocale();
- } catch (IOException | InvalidConfigurationException e) {
- e.printStackTrace();
- }
- }
-
-
- private void populateLocale() {
- Set missing = new HashSet<>(Arrays.asList(LocaleMessage.values()));
- for (LocaleMessage localeMessage : LocaleMessage.values()) {
- if (locale.contains(localeMessage.getPath())) {
- localeMessage.setMessage(locale.getString(localeMessage.getPath()));
- missing.remove(localeMessage);
- }
- }
-
- for (LocaleMessage localeMessage : missing) {
- locale.set(localeMessage.getPath(), localeMessage.getMessage());
- }
-
-
- }
-
- private void sortLocale() {
- FileConfiguration newLocale = new YamlConfiguration();
- List keys = new ArrayList<>();
- keys.addAll(locale.getKeys(true));
- Collections.sort(keys);
- for (String key : keys) {
- newLocale.set(key, locale.getString(key));
- }
- locale = newLocale;
- }
-
- private void saveLocale() {
- try {
- locale.save(dataFile);
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
-
-}
diff --git a/src/main/java/simplexity/simplenicks/listener/QuitListener.java b/src/main/java/simplexity/simplenicks/listener/QuitListener.java
deleted file mode 100644
index f76c9a3..0000000
--- a/src/main/java/simplexity/simplenicks/listener/QuitListener.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package simplexity.simplenicks.listener;
-
-import org.bukkit.entity.Player;
-import org.bukkit.event.EventHandler;
-import org.bukkit.event.Listener;
-import org.bukkit.event.player.PlayerQuitEvent;
-import simplexity.simplenicks.saving.Cache;
-
-public class QuitListener implements Listener {
-
- @EventHandler
- public void onPlayerQuit(PlayerQuitEvent quitEvent) {
- Player player = quitEvent.getPlayer();
- Cache.getInstance().removePlayerFromCache(player.getUniqueId());
- }
-}
diff --git a/src/main/java/simplexity/simplenicks/util/ColorTag.java b/src/main/java/simplexity/simplenicks/util/ColorTag.java
deleted file mode 100644
index d626ee6..0000000
--- a/src/main/java/simplexity/simplenicks/util/ColorTag.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package simplexity.simplenicks.util;
-
-import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
-import net.kyori.adventure.text.minimessage.tag.standard.StandardTags;
-import org.bukkit.permissions.Permission;
-import org.bukkit.permissions.PermissionDefault;
-import org.jetbrains.annotations.NotNull;
-
-public enum ColorTag {
- //Nickname Perms
- HEX_COLOR(new Permission("simplenick.color.basic", PermissionDefault.OP), StandardTags.color()),
- GRADIENT(new Permission("simplenick.color.gradient", PermissionDefault.OP), StandardTags.gradient()),
- RAINBOW(new Permission("simplenick.color.rainbow", PermissionDefault.OP), StandardTags.rainbow()),
- RESET(new Permission("simplenick.color.reset", PermissionDefault.OP), StandardTags.reset());
-
-
- private final Permission permission;
- private final TagResolver resolver;
-
-
- ColorTag(Permission permission, TagResolver resolver) {
- this.permission = permission;
- this.resolver = resolver;
- }
-
- @NotNull
- public Permission getPermission() {
- return permission;
- }
-
- @NotNull
- public TagResolver getTagResolver() {
- return resolver;
- }
-}
diff --git a/src/main/java/simplexity/simplenicks/util/FormatTag.java b/src/main/java/simplexity/simplenicks/util/FormatTag.java
deleted file mode 100644
index 2ac9335..0000000
--- a/src/main/java/simplexity/simplenicks/util/FormatTag.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package simplexity.simplenicks.util;
-
-import net.kyori.adventure.text.format.TextDecoration;
-import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
-import net.kyori.adventure.text.minimessage.tag.standard.StandardTags;
-import org.bukkit.permissions.Permission;
-import org.bukkit.permissions.PermissionDefault;
-import org.jetbrains.annotations.NotNull;
-
-public enum FormatTag {
-
- UNDERLINE(new Permission("simplenick.format.underline", PermissionDefault.OP), StandardTags.decorations(TextDecoration.UNDERLINED)),
- ITALIC(new Permission("simplenick.format.italic", PermissionDefault.OP), StandardTags.decorations(TextDecoration.ITALIC)),
- STRIKETHROUGH(new Permission("simplenick.format.strikethrough", PermissionDefault.OP), StandardTags.decorations(TextDecoration.STRIKETHROUGH)),
- BOLD(new Permission("simplenick.format.bold", PermissionDefault.OP), StandardTags.decorations(TextDecoration.BOLD)),
- OBFUSCATED(new Permission("simplenick.format.obfuscated", PermissionDefault.OP), StandardTags.decorations(TextDecoration.OBFUSCATED)),
- HOVER(new Permission("simplenick.format.hover", PermissionDefault.FALSE), StandardTags.hoverEvent()),
- FONT(new Permission("simplenick.format.font", PermissionDefault.FALSE), StandardTags.font()),;
-
-
- private final Permission permission;
- private final TagResolver resolver;
-
-
- FormatTag(Permission permission, TagResolver resolver) {
- this.permission = permission;
- this.resolver = resolver;
- }
-
- @NotNull
- public Permission getPermission() {
- return permission;
- }
-
- @NotNull
- public TagResolver getTagResolver() {
- return resolver;
- }
-}
diff --git a/src/main/java/simplexity/simplenicks/util/NickPermission.java b/src/main/java/simplexity/simplenicks/util/NickPermission.java
deleted file mode 100644
index 2f2436c..0000000
--- a/src/main/java/simplexity/simplenicks/util/NickPermission.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package simplexity.simplenicks.util;
-
-import org.bukkit.permissions.Permission;
-import org.bukkit.permissions.PermissionDefault;
-import org.jetbrains.annotations.NotNull;
-
-public enum NickPermission {
- // name, description, default, children
- NICK_ADMIN(new Permission("simplenick.admin", "Base permission for all admin commands", PermissionDefault.OP)),
- NICK_ADMIN_SET(new Permission("simplenick.admin.set", "Allows an admin to set another user's nickname", PermissionDefault.OP)),
- NICK_ADMIN_RESET(new Permission("simplenick.admin.reset", "Allows an admin to reset another user's nickname", PermissionDefault.OP)),
- NICK_ADMIN_DELETE(new Permission("simplenick.admin.delete", "Allows an admin to delete another user's saved nickname", PermissionDefault.OP)),
- NICK_ADMIN_LOOKUP(new Permission("simplenick.admin.lookup", "Allows an admin to look up someone's nickname and saved nicknames based off their username", PermissionDefault.OP)),
- NICK_COMMAND(new Permission("simplenick.nick", "Base permission for all nickname commands", PermissionDefault.TRUE)),
- NICK_SET(new Permission("simplenick.nick.set", "Allows someone to set their own nickname", PermissionDefault.OP)),
- NICK_SAVE(new Permission("simplenick.nick.save", "Allows someone to save nicknames", PermissionDefault.OP)),
- NICK_WHO(new Permission("simplenick.nick.who", "Allows someone to see the actual username of someone based on their nickname", PermissionDefault.TRUE)),
- NICK_HELP(new Permission("simplenick.nick.help", "Shows the help messages", PermissionDefault.TRUE)),
- NICK_BYPASS_USERNAME(new Permission("simplenick.bypass.username", "Allows a user to nickname themselves the same as someone else's username on this server", PermissionDefault.FALSE)),
- NICK_BYPASS_LENGTH(new Permission("simplenick.bypass.length", "Allows a user to bypass the configured max length of a nickname", PermissionDefault.FALSE)),
- NICK_BYPASS_REGEX(new Permission("simplenick.bypass.regex", "Allows a user to bypass the configured regex", PermissionDefault.FALSE)),
- NICK_BYPASS_NICK_PROTECTION(new Permission("simplenick.bypass.nick-protection", "Allows a user to nickname themselves the same nickname as another user", PermissionDefault.FALSE)),
- NICK_RELOAD(new Permission("simplenick.reload", "Allows a user to reload the config", PermissionDefault.OP));
-
- private final Permission permission;
-
- NickPermission(Permission permission) {
- this.permission = permission;
- }
-
- @NotNull
- public Permission getPermission() {
- return permission;
- }
-
-}