diff --git a/.gitignore b/.gitignore
index 4788b4b..dd0ea15 100644
--- a/.gitignore
+++ b/.gitignore
@@ -95,19 +95,13 @@ $RECYCLE.BIN/
# Windows shortcuts
*.lnk
+# Maven
target/
-pom.xml.tag
-pom.xml.releaseBackup
-pom.xml.versionsBackup
-pom.xml.next
-
-release.properties
-dependency-reduced-pom.xml
-buildNumber.properties
-.mvn/timing.properties
-.mvn/wrapper/maven-wrapper.jar
-.flattened-pom.xml
+# Gradle
+.gradle/
+**/build/
+**/.gradle/
# Common working directory
run/
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..6d0aa63
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,23 @@
+allprojects {
+ group = 'simplexity'
+ version = '3.2.2'
+}
+
+subprojects {
+ apply plugin: 'java'
+
+ java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(25)
+ }
+ }
+
+ repositories {
+ mavenCentral()
+ maven { url 'https://repo.papermc.io/repository/maven-public/' }
+ maven { url 'https://maven.fabricmc.net/' }
+ maven { url 'https://repo.extendedclip.com/content/repositories/placeholderapi/' }
+ maven { url 'https://oss.sonatype.org/content/groups/public/' }
+ maven { url 'https://maven.nucleoid.xyz/' }
+ }
+}
diff --git a/core/build.gradle b/core/build.gradle
new file mode 100644
index 0000000..2abfca6
--- /dev/null
+++ b/core/build.gradle
@@ -0,0 +1,21 @@
+plugins {
+ id 'java-library'
+}
+
+java {
+ targetCompatibility = JavaVersion.VERSION_21
+ sourceCompatibility = JavaVersion.VERSION_21
+}
+
+tasks.withType(JavaCompile).configureEach {
+ options.release = 21
+}
+
+dependencies {
+ compileOnly "net.kyori:adventure-api:${adventureVersion}"
+ compileOnly "net.kyori:adventure-text-minimessage:${miniMessageVersion}"
+ compileOnly "org.slf4j:slf4j-api:${slf4jVersion}"
+ api "com.zaxxer:HikariCP:${hikariVersion}"
+ compileOnly "org.jetbrains:annotations:24.0.0"
+ compileOnly "com.mojang:brigadier:1.3.10"
+}
diff --git a/core/src/main/java/simplexity/simplenicks/SimpleNicksCore.java b/core/src/main/java/simplexity/simplenicks/SimpleNicksCore.java
new file mode 100644
index 0000000..d9ad9f4
--- /dev/null
+++ b/core/src/main/java/simplexity/simplenicks/SimpleNicksCore.java
@@ -0,0 +1,75 @@
+package simplexity.simplenicks;
+
+import net.kyori.adventure.text.minimessage.MiniMessage;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.platform.PlatformAdapter;
+
+/**
+ * Central singleton for the SimpleNicks core.
+ *
+ * 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..94af363
--- /dev/null
+++ b/core/src/main/java/simplexity/simplenicks/commands/subcommands/Exceptions.java
@@ -0,0 +1,101 @@
+package simplexity.simplenicks.commands.subcommands;
+
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.config.ConfigHandler;
+import simplexity.simplenicks.config.LocaleMessage;
+import simplexity.simplenicks.platform.BrigadierAdapter;
+
+/**
+ * Factory methods for Brigadier {@link CommandSyntaxException} instances.
+ *
+ * All methods accept a {@link BrigadierAdapter} so exception messages are rendered as styled
+ * Adventure components rather than plain text. Methods are used instead of static fields to
+ * avoid class-load timing issues with {@link SimpleNicksCore}.
+ *
+ */
+public class Exceptions {
+
+ private static Component deserializeComponent(@NotNull String miniMessage) {
+ return SimpleNicksCore.get().miniMessage().deserialize(miniMessage);
+ }
+
+ public static CommandSyntaxException nickIsNull(@NotNull BrigadierAdapter adapter) {
+ return new SimpleCommandExceptionType(
+ adapter.brigadierMessage(deserializeComponent(LocaleMessage.ERROR_NICK_IS_NULL.getMessage()))
+ ).create();
+ }
+
+ public static CommandSyntaxException emptyNickAfterParse(@NotNull BrigadierAdapter adapter) {
+ return new SimpleCommandExceptionType(
+ adapter.brigadierMessage(deserializeComponent(LocaleMessage.ERROR_INVALID_NICK_EMPTY.getMessage()))
+ ).create();
+ }
+
+ public static CommandSyntaxException cannotSave(@NotNull BrigadierAdapter adapter) {
+ return new SimpleCommandExceptionType(
+ adapter.brigadierMessage(deserializeComponent(LocaleMessage.ERROR_SAVE_FAILURE.getMessage()))
+ ).create();
+ }
+
+ public static CommandSyntaxException tooManySavedNames(@NotNull BrigadierAdapter adapter) {
+ return new SimpleCommandExceptionType(
+ adapter.brigadierMessage(deserializeComponent(LocaleMessage.ERROR_TOO_MANY_TO_SAVE.getMessage()))
+ ).create();
+ }
+
+ public static CommandSyntaxException tagsNotPermitted(@NotNull BrigadierAdapter adapter) {
+ return new SimpleCommandExceptionType(
+ adapter.brigadierMessage(deserializeComponent(LocaleMessage.ERROR_INVALID_TAGS.getMessage()))
+ ).create();
+ }
+
+ public static CommandSyntaxException alreadySaved(@NotNull BrigadierAdapter adapter) {
+ return new SimpleCommandExceptionType(
+ adapter.brigadierMessage(deserializeComponent(LocaleMessage.ERROR_ALREADY_SAVED.getMessage()))
+ ).create();
+ }
+
+ public static CommandSyntaxException lengthError(@NotNull BrigadierAdapter adapter, Object nickname) {
+ Component message = SimpleNicksCore.get().miniMessage().deserialize(
+ LocaleMessage.ERROR_INVALID_NICK_LENGTH.getMessage(),
+ Placeholder.unparsed("value", String.valueOf(ConfigHandler.getInstance().getMaxLength())),
+ Placeholder.unparsed("name", String.valueOf(nickname)));
+ return new SimpleCommandExceptionType(adapter.brigadierMessage(message)).create();
+ }
+
+ public static CommandSyntaxException regexError(@NotNull BrigadierAdapter adapter, Object nickname) {
+ Component message = SimpleNicksCore.get().miniMessage().deserialize(
+ LocaleMessage.ERROR_INVALID_NICK.getMessage(),
+ Placeholder.unparsed("regex", ConfigHandler.getInstance().getRegexString()));
+ return new SimpleCommandExceptionType(adapter.brigadierMessage(message)).create();
+ }
+
+ public static CommandSyntaxException invalidPlayerSpecified(
+ @NotNull BrigadierAdapter adapter, Object playerName) {
+ Component message = SimpleNicksCore.get().miniMessage().deserialize(
+ LocaleMessage.ERROR_INVALID_PLAYER.getMessage(),
+ Placeholder.unparsed("player_name", String.valueOf(playerName)));
+ return new SimpleCommandExceptionType(adapter.brigadierMessage(message)).create();
+ }
+
+ public static CommandSyntaxException nicknameSomeonesUsername(
+ @NotNull BrigadierAdapter adapter, Object nickname) {
+ Component message = SimpleNicksCore.get().miniMessage().deserialize(
+ LocaleMessage.ERROR_INVALID_OTHER_PLAYERS_USERNAME.getMessage(),
+ Placeholder.unparsed("value", String.valueOf(nickname)));
+ return new SimpleCommandExceptionType(adapter.brigadierMessage(message)).create();
+ }
+
+ public static CommandSyntaxException someoneUsingThatNickname(
+ @NotNull BrigadierAdapter adapter, Object nickname) {
+ Component message = SimpleNicksCore.get().miniMessage().deserialize(
+ LocaleMessage.ERROR_INVALID_OTHER_PLAYERS_NICKNAME.getMessage(),
+ Placeholder.unparsed("value", String.valueOf(nickname)));
+ return new SimpleCommandExceptionType(adapter.brigadierMessage(message)).create();
+ }
+}
diff --git a/core/src/main/java/simplexity/simplenicks/commands/subcommands/NickSuggestionProviders.java b/core/src/main/java/simplexity/simplenicks/commands/subcommands/NickSuggestionProviders.java
new file mode 100644
index 0000000..ae2bcfa
--- /dev/null
+++ b/core/src/main/java/simplexity/simplenicks/commands/subcommands/NickSuggestionProviders.java
@@ -0,0 +1,56 @@
+package simplexity.simplenicks.commands.subcommands;
+
+import com.mojang.brigadier.suggestion.SuggestionProvider;
+import com.mojang.brigadier.suggestion.Suggestions;
+import com.mojang.brigadier.suggestion.SuggestionsBuilder;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.commands.NicknameProcessor;
+import simplexity.simplenicks.platform.BrigadierAdapter;
+import simplexity.simplenicks.saving.Cache;
+import simplexity.simplenicks.saving.Nickname;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Reusable Brigadier suggestion providers for nickname and player arguments.
+ */
+public final class NickSuggestionProviders {
+
+ private NickSuggestionProviders() {}
+
+ /**
+ * Suggests the calling player's own saved nicknames.
+ * Used by {@code /nick delete} and as Fabric's fallback for {@code /nick set}.
+ *
+ * @param adapter the adapter used to resolve the player UUID from the source
+ * @param the platform's CommandSourceStack type
+ * @return a suggestion provider
+ */
+ @NotNull
+ public static SuggestionProvider ownSavedNicks(@NotNull BrigadierAdapter adapter) {
+ return (ctx, builder) -> {
+ adapter.getPlayerUuid(ctx.getSource()).ifPresent(uuid -> {
+ List saved = NicknameProcessor.getInstance().getSavedNicknames(uuid, true);
+ saved.forEach(savedNickname -> builder.suggest(savedNickname.getNickname()));
+ });
+ return builder.buildFuture();
+ };
+ }
+
+ /**
+ * Suggests the normalized nicknames of all online players that currently have an active nickname.
+ * Used by {@code /nick who}.
+ *
+ * @param the platform's CommandSourceStack type
+ * @return a suggestion provider
+ */
+ @NotNull
+ public static SuggestionProvider activeOnlineNicks() {
+ return (ctx, builder) -> {
+ Cache.getInstance().getOnlineNicknames().values()
+ .forEach(activeNickname -> builder.suggest(activeNickname.getNormalizedNickname()));
+ return builder.buildFuture();
+ };
+ }
+}
diff --git a/core/src/main/java/simplexity/simplenicks/commands/subcommands/SubCommand.java b/core/src/main/java/simplexity/simplenicks/commands/subcommands/SubCommand.java
new file mode 100644
index 0000000..13ee380
--- /dev/null
+++ b/core/src/main/java/simplexity/simplenicks/commands/subcommands/SubCommand.java
@@ -0,0 +1,55 @@
+package simplexity.simplenicks.commands.subcommands;
+
+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 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.logic.NickUtils;
+import simplexity.simplenicks.platform.SenderContext;
+import simplexity.simplenicks.saving.Nickname;
+
+import java.util.UUID;
+
+public interface SubCommand {
+
+ void subcommandTo(@NotNull LiteralArgumentBuilder parent);
+
+ int execute(@NotNull CommandContext ctx) throws CommandSyntaxException;
+
+ boolean canExecute(@NotNull S 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));
+ }
+
+ default void sendFeedback(@NotNull SenderContext sender, @Nullable LocaleMessage msg, @Nullable Nickname nick) {
+ if (nick == null) nick = new Nickname("", "");
+ if (msg == null || msg.getMessage().isEmpty()) return;
+ Component component = SimpleNicksCore.get().miniMessage().deserialize(
+ msg.getMessage(), Placeholder.parsed("value", nick.getNickname()));
+ sender.sendMessage(component);
+ }
+
+ default @NotNull Component parseAdminMessage(@NotNull String template, @NotNull String value,
+ @NotNull SenderContext initiator, @NotNull String targetName) {
+ String initiatorDisplayName = initiator.isPlayer()
+ ? initiator.getDisplayName()
+ : LocaleMessage.SERVER_DISPLAY_NAME.getMessage();
+ Component initiatorComponent = SimpleNicksCore.get().miniMessage().deserialize(initiatorDisplayName);
+ return SimpleNicksCore.get().miniMessage().deserialize(template,
+ Placeholder.parsed("value", value),
+ Placeholder.component("initiator", initiatorComponent),
+ Placeholder.parsed("target", targetName));
+ }
+}
diff --git a/core/src/main/java/simplexity/simplenicks/commands/subcommands/admin/AdminDeleteSubCommand.java b/core/src/main/java/simplexity/simplenicks/commands/subcommands/admin/AdminDeleteSubCommand.java
new file mode 100644
index 0000000..dec19f7
--- /dev/null
+++ b/core/src/main/java/simplexity/simplenicks/commands/subcommands/admin/AdminDeleteSubCommand.java
@@ -0,0 +1,67 @@
+package simplexity.simplenicks.commands.subcommands.admin;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.commands.NicknameProcessor;
+import simplexity.simplenicks.commands.subcommands.Exceptions;
+import simplexity.simplenicks.commands.subcommands.SubCommand;
+import simplexity.simplenicks.config.LocaleMessage;
+import simplexity.simplenicks.logic.NickUtils;
+import simplexity.simplenicks.platform.BrigadierAdapter;
+import simplexity.simplenicks.platform.PlayerTarget;
+import simplexity.simplenicks.platform.SenderContext;
+import simplexity.simplenicks.saving.Nickname;
+import simplexity.simplenicks.util.NickPermission;
+
+public class AdminDeleteSubCommand implements SubCommand {
+
+ private final BrigadierAdapter adapter;
+
+ public AdminDeleteSubCommand(@NotNull BrigadierAdapter adapter) {
+ this.adapter = adapter;
+ }
+
+ @Override
+ public void subcommandTo(@NotNull LiteralArgumentBuilder parent) {
+ parent.then(adapter.literal("delete").requires(this::canExecute)
+ .then(adapter.playerThenTargetNicknameArgument("player", "nickname", this::execute)));
+ }
+
+ @Override
+ public int execute(@NotNull CommandContext ctx) throws CommandSyntaxException {
+ S source = ctx.getSource();
+ SenderContext senderCtx = adapter.senderContext(source);
+ PlayerTarget target = adapter.getPlayerTarget(ctx, "player")
+ .orElseThrow(() -> Exceptions.invalidPlayerSpecified(adapter, ""));
+ Nickname nickname = adapter.getNickname(ctx, "nickname");
+ String targetName = !target.name().isEmpty() ? target.name() : "[Username not found]";
+ SimpleNicksCore.get().platform().runAsync(() -> {
+ boolean success = NicknameProcessor.getInstance()
+ .deleteNickname(target.id(), nickname.getNickname());
+ if (success) {
+ SimpleNicksCore.get().platform().runSync(() -> {
+ if (SimpleNicksCore.get().platform().isPlayerOnline(target.id())) {
+ NickUtils.refreshDisplayName(target.id());
+ SimpleNicksCore.get().platform().sendMessageToPlayer(target.id(),
+ parseAdminMessage(LocaleMessage.DELETED_BY_INITIATOR.getMessage(),
+ nickname.getNickname(), senderCtx, targetName));
+ }
+ senderCtx.sendMessage(parseAdminMessage(LocaleMessage.DELETED_TARGET.getMessage(),
+ nickname.getNickname(), senderCtx, targetName));
+ });
+ } else {
+ sendFeedback(senderCtx, LocaleMessage.ERROR_DELETE_FAILURE, nickname);
+ }
+ });
+ return Command.SINGLE_SUCCESS;
+ }
+
+ @Override
+ public boolean canExecute(@NotNull S source) {
+ return adapter.hasPermission(source, NickPermission.NICK_ADMIN_DELETE);
+ }
+}
diff --git a/core/src/main/java/simplexity/simplenicks/commands/subcommands/admin/AdminLookupSubCommand.java b/core/src/main/java/simplexity/simplenicks/commands/subcommands/admin/AdminLookupSubCommand.java
new file mode 100644
index 0000000..d8e4d5a
--- /dev/null
+++ b/core/src/main/java/simplexity/simplenicks/commands/subcommands/admin/AdminLookupSubCommand.java
@@ -0,0 +1,82 @@
+package simplexity.simplenicks.commands.subcommands.admin;
+
+import com.mojang.brigadier.Command;
+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 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.commands.subcommands.SubCommand;
+import simplexity.simplenicks.config.LocaleMessage;
+import simplexity.simplenicks.config.MessageUtils;
+import simplexity.simplenicks.platform.BrigadierAdapter;
+import simplexity.simplenicks.platform.PlayerTarget;
+import simplexity.simplenicks.platform.SenderContext;
+import simplexity.simplenicks.saving.Nickname;
+import simplexity.simplenicks.util.NickPermission;
+
+import java.util.List;
+
+public class AdminLookupSubCommand implements SubCommand {
+
+ private final BrigadierAdapter adapter;
+
+ public AdminLookupSubCommand(@NotNull BrigadierAdapter adapter) {
+ this.adapter = adapter;
+ }
+
+ @Override
+ public void subcommandTo(@NotNull LiteralArgumentBuilder parent) {
+ parent.then(adapter.literal("lookup").requires(this::canExecute)
+ .then(adapter.playerArgument("player", this::execute)));
+ }
+
+ @Override
+ public int execute(@NotNull CommandContext ctx) throws CommandSyntaxException {
+ S source = ctx.getSource();
+ SenderContext senderCtx = adapter.senderContext(source);
+ PlayerTarget target = adapter.getPlayerTarget(ctx, "player")
+ .orElseThrow(() -> Exceptions.invalidPlayerSpecified(adapter, ""));
+ boolean isOnline = SimpleNicksCore.get().platform().isPlayerOnline(target.id());
+ SimpleNicksCore.get().platform().runAsync(() -> {
+ Nickname currentNickname = NicknameProcessor.getInstance()
+ .getCurrentNickname(target.id(), isOnline);
+ List savedNicknames = NicknameProcessor.getInstance()
+ .getSavedNicknames(target.id(), isOnline);
+ senderCtx.sendMessage(lookupInfoComponent(target.name(), currentNickname, savedNicknames));
+ });
+ return Command.SINGLE_SUCCESS;
+ }
+
+ @Override
+ public boolean canExecute(@NotNull S source) {
+ return adapter.hasPermission(source, NickPermission.NICK_ADMIN_LOOKUP);
+ }
+
+ @NotNull
+ private Component lookupInfoComponent(@NotNull String username, @Nullable Nickname currentNick,
+ @Nullable List savedNames) {
+ String nickname;
+ if (currentNick == null) {
+ if (savedNames == null || savedNames.isEmpty()) {
+ return SimpleNicksCore.get().miniMessage()
+ .deserialize(LocaleMessage.ERROR_NO_PLAYERS_WITH_THIS_NAME.getMessage());
+ }
+ nickname = LocaleMessage.INSERT_NONE.getMessage();
+ } else {
+ nickname = currentNick.getNickname();
+ }
+ String infoString = LocaleMessage.LOOKUP_HEADER.getMessage()
+ + LocaleMessage.LOOKUP_CURRENT.getMessage()
+ + LocaleMessage.LOOKUP_SAVED.getMessage();
+ return SimpleNicksCore.get().miniMessage().deserialize(infoString,
+ Placeholder.unparsed("username", username),
+ Placeholder.parsed("name", nickname),
+ MessageUtils.savedNickListResolver(savedNames));
+ }
+}
diff --git a/core/src/main/java/simplexity/simplenicks/commands/subcommands/admin/AdminResetSubCommand.java b/core/src/main/java/simplexity/simplenicks/commands/subcommands/admin/AdminResetSubCommand.java
new file mode 100644
index 0000000..b3ddd86
--- /dev/null
+++ b/core/src/main/java/simplexity/simplenicks/commands/subcommands/admin/AdminResetSubCommand.java
@@ -0,0 +1,64 @@
+package simplexity.simplenicks.commands.subcommands.admin;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.commands.NicknameProcessor;
+import simplexity.simplenicks.commands.subcommands.Exceptions;
+import simplexity.simplenicks.commands.subcommands.SubCommand;
+import simplexity.simplenicks.config.LocaleMessage;
+import simplexity.simplenicks.logic.NickUtils;
+import simplexity.simplenicks.platform.BrigadierAdapter;
+import simplexity.simplenicks.platform.PlayerTarget;
+import simplexity.simplenicks.platform.SenderContext;
+import simplexity.simplenicks.util.NickPermission;
+
+public class AdminResetSubCommand implements SubCommand {
+
+ private final BrigadierAdapter adapter;
+
+ public AdminResetSubCommand(@NotNull BrigadierAdapter adapter) {
+ this.adapter = adapter;
+ }
+
+ @Override
+ public void subcommandTo(@NotNull LiteralArgumentBuilder parent) {
+ parent.then(adapter.literal("reset").requires(this::canExecute)
+ .then(adapter.playerArgument("player", this::execute)));
+ }
+
+ @Override
+ public int execute(@NotNull CommandContext ctx) throws CommandSyntaxException {
+ S source = ctx.getSource();
+ SenderContext senderCtx = adapter.senderContext(source);
+ PlayerTarget target = adapter.getPlayerTarget(ctx, "player")
+ .orElseThrow(() -> Exceptions.invalidPlayerSpecified(adapter, ""));
+ String targetName = !target.name().isEmpty() ? target.name() : "[Username not found]";
+ SimpleNicksCore.get().platform().runAsync(() -> {
+ boolean success = NicknameProcessor.getInstance().resetNickname(target.id());
+ if (success) {
+ SimpleNicksCore.get().platform().runSync(() -> {
+ if (SimpleNicksCore.get().platform().isPlayerOnline(target.id())) {
+ NickUtils.refreshDisplayName(target.id());
+ SimpleNicksCore.get().platform().sendMessageToPlayer(target.id(),
+ parseAdminMessage(LocaleMessage.RESET_BY_INITIATOR.getMessage(),
+ "", senderCtx, targetName));
+ }
+ senderCtx.sendMessage(parseAdminMessage(LocaleMessage.RESET_TARGET.getMessage(),
+ "", senderCtx, targetName));
+ });
+ } else {
+ sendFeedback(senderCtx, LocaleMessage.ERROR_RESET_FAILURE, null);
+ }
+ });
+ return Command.SINGLE_SUCCESS;
+ }
+
+ @Override
+ public boolean canExecute(@NotNull S source) {
+ return adapter.hasPermission(source, NickPermission.NICK_ADMIN_RESET);
+ }
+}
diff --git a/core/src/main/java/simplexity/simplenicks/commands/subcommands/admin/AdminSetSubCommand.java b/core/src/main/java/simplexity/simplenicks/commands/subcommands/admin/AdminSetSubCommand.java
new file mode 100644
index 0000000..a25d08e
--- /dev/null
+++ b/core/src/main/java/simplexity/simplenicks/commands/subcommands/admin/AdminSetSubCommand.java
@@ -0,0 +1,68 @@
+package simplexity.simplenicks.commands.subcommands.admin;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.commands.NicknameProcessor;
+import simplexity.simplenicks.commands.subcommands.Exceptions;
+import simplexity.simplenicks.commands.subcommands.SubCommand;
+import simplexity.simplenicks.config.LocaleMessage;
+import simplexity.simplenicks.logic.NickUtils;
+import simplexity.simplenicks.platform.BrigadierAdapter;
+import simplexity.simplenicks.platform.PlayerTarget;
+import simplexity.simplenicks.platform.SenderContext;
+import simplexity.simplenicks.saving.Nickname;
+import simplexity.simplenicks.util.NickPermission;
+
+public class AdminSetSubCommand implements SubCommand {
+
+ private final BrigadierAdapter adapter;
+
+ public AdminSetSubCommand(@NotNull BrigadierAdapter adapter) {
+ this.adapter = adapter;
+ }
+
+ @Override
+ public void subcommandTo(@NotNull LiteralArgumentBuilder parent) {
+ parent.then(adapter.literal("set").requires(this::canExecute)
+ .then(adapter.playerThenNicknameArgument("player", "nickname", this::execute)));
+ }
+
+ @Override
+ public int execute(@NotNull CommandContext ctx) throws CommandSyntaxException {
+ S source = ctx.getSource();
+ SenderContext senderCtx = adapter.senderContext(source);
+ PlayerTarget target = adapter.getPlayerTarget(ctx, "player")
+ .orElseThrow(() -> Exceptions.invalidPlayerSpecified(adapter, ""));
+ Nickname nickname = adapter.getNickname(ctx, "nickname");
+ if (!NickUtils.isValidTags(senderCtx, nickname.getNickname())) throw Exceptions.tagsNotPermitted(adapter);
+ NickUtils.nicknameChecks(adapter, senderCtx, nickname);
+ SimpleNicksCore.get().platform().runAsync(() -> {
+ boolean success = NicknameProcessor.getInstance()
+ .setNickname(target.id(), target.name(), nickname.getNickname());
+ if (success) {
+ SimpleNicksCore.get().platform().runSync(() -> {
+ if (SimpleNicksCore.get().platform().isPlayerOnline(target.id())) {
+ NickUtils.refreshDisplayName(target.id());
+ SimpleNicksCore.get().platform().sendMessageToPlayer(target.id(),
+ parseAdminMessage(LocaleMessage.SET_BY_INITIATOR.getMessage(),
+ nickname.getNickname(), senderCtx, target.name()));
+ }
+ senderCtx.sendMessage(parseAdminMessage(LocaleMessage.SET_TARGET.getMessage(),
+ nickname.getNickname(), senderCtx, target.name()));
+ });
+ } else {
+ sendFeedback(senderCtx, LocaleMessage.ERROR_SET_FAILURE, nickname);
+ }
+ });
+ return Command.SINGLE_SUCCESS;
+ }
+
+ @Override
+ public boolean canExecute(@NotNull S source) {
+ return adapter.hasPermission(source, NickPermission.NICK_ADMIN_SET);
+ }
+}
diff --git a/core/src/main/java/simplexity/simplenicks/commands/subcommands/admin/AdminSubCommand.java b/core/src/main/java/simplexity/simplenicks/commands/subcommands/admin/AdminSubCommand.java
new file mode 100644
index 0000000..c39600d
--- /dev/null
+++ b/core/src/main/java/simplexity/simplenicks/commands/subcommands/admin/AdminSubCommand.java
@@ -0,0 +1,37 @@
+package simplexity.simplenicks.commands.subcommands.admin;
+
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.commands.subcommands.SubCommand;
+import simplexity.simplenicks.platform.BrigadierAdapter;
+import simplexity.simplenicks.util.NickPermission;
+
+public class AdminSubCommand implements SubCommand {
+
+ private final BrigadierAdapter adapter;
+
+ public AdminSubCommand(@NotNull BrigadierAdapter adapter) {
+ this.adapter = adapter;
+ }
+
+ @Override
+ public void subcommandTo(@NotNull LiteralArgumentBuilder parent) {
+ LiteralArgumentBuilder admin = adapter.literal("admin").requires(this::canExecute);
+ new AdminSetSubCommand<>(adapter).subcommandTo(admin);
+ new AdminResetSubCommand<>(adapter).subcommandTo(admin);
+ new AdminLookupSubCommand<>(adapter).subcommandTo(admin);
+ new AdminDeleteSubCommand<>(adapter).subcommandTo(admin);
+ parent.then(admin);
+ }
+
+ @Override
+ public int execute(@NotNull CommandContext ctx) {
+ throw new UnsupportedOperationException("AdminSubCommand::execute was used, this should be impossible.");
+ }
+
+ @Override
+ public boolean canExecute(@NotNull S source) {
+ return adapter.hasPermission(source, NickPermission.NICK_ADMIN);
+ }
+}
diff --git a/core/src/main/java/simplexity/simplenicks/commands/subcommands/basic/DeleteSubCommand.java b/core/src/main/java/simplexity/simplenicks/commands/subcommands/basic/DeleteSubCommand.java
new file mode 100644
index 0000000..c92fc1c
--- /dev/null
+++ b/core/src/main/java/simplexity/simplenicks/commands/subcommands/basic/DeleteSubCommand.java
@@ -0,0 +1,62 @@
+package simplexity.simplenicks.commands.subcommands.basic;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.commands.NicknameProcessor;
+import simplexity.simplenicks.commands.subcommands.NickSuggestionProviders;
+import simplexity.simplenicks.commands.subcommands.SubCommand;
+import simplexity.simplenicks.config.LocaleMessage;
+import simplexity.simplenicks.logic.NickUtils;
+import simplexity.simplenicks.platform.BrigadierAdapter;
+import simplexity.simplenicks.platform.SenderContext;
+import simplexity.simplenicks.saving.Nickname;
+import simplexity.simplenicks.util.NickPermission;
+
+import java.util.UUID;
+
+public class DeleteSubCommand implements SubCommand {
+
+ private final BrigadierAdapter adapter;
+
+ public DeleteSubCommand(@NotNull BrigadierAdapter adapter) {
+ this.adapter = adapter;
+ }
+
+ @Override
+ public void subcommandTo(@NotNull LiteralArgumentBuilder parent) {
+ parent.then(adapter.literal("delete").requires(this::canExecute)
+ .then(adapter.nicknameArgument("nickname",
+ NickSuggestionProviders.ownSavedNicks(adapter),
+ this::execute)));
+ }
+
+ @Override
+ public int execute(@NotNull CommandContext ctx) throws CommandSyntaxException {
+ S source = ctx.getSource();
+ UUID uuid = adapter.getPlayerUuid(source).orElseThrow();
+ SenderContext sender = adapter.senderContext(source);
+ Nickname nickname = adapter.getNickname(ctx, "nickname");
+ SimpleNicksCore.get().platform().runAsync(() -> {
+ boolean success = NicknameProcessor.getInstance().deleteNickname(uuid, nickname.getNickname());
+ if (success) {
+ SimpleNicksCore.get().platform().runSync(() -> {
+ NickUtils.refreshDisplayName(uuid);
+ sendFeedback(sender, LocaleMessage.DELETE_SELF, nickname);
+ });
+ } else {
+ sendFeedback(sender, LocaleMessage.ERROR_DELETE_FAILURE, nickname);
+ }
+ });
+ return Command.SINGLE_SUCCESS;
+ }
+
+ @Override
+ public boolean canExecute(@NotNull S source) {
+ return adapter.isPlayer(source)
+ && (permissionNotRequired() || adapter.hasPermission(source, NickPermission.NICK_SAVE));
+ }
+}
diff --git a/core/src/main/java/simplexity/simplenicks/commands/subcommands/basic/HelpSubCommand.java b/core/src/main/java/simplexity/simplenicks/commands/subcommands/basic/HelpSubCommand.java
new file mode 100644
index 0000000..d84ff4b
--- /dev/null
+++ b/core/src/main/java/simplexity/simplenicks/commands/subcommands/basic/HelpSubCommand.java
@@ -0,0 +1,90 @@
+package simplexity.simplenicks.commands.subcommands.basic;
+
+import com.mojang.brigadier.Command;
+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 org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.commands.subcommands.SubCommand;
+import simplexity.simplenicks.config.ConfigHandler;
+import simplexity.simplenicks.config.LocaleMessage;
+import simplexity.simplenicks.platform.BrigadierAdapter;
+import simplexity.simplenicks.platform.SenderContext;
+import simplexity.simplenicks.util.NickPermission;
+
+public class HelpSubCommand implements SubCommand {
+
+ private final BrigadierAdapter adapter;
+
+ public HelpSubCommand(@NotNull BrigadierAdapter adapter) {
+ this.adapter = adapter;
+ }
+
+ @Override
+ public void subcommandTo(@NotNull LiteralArgumentBuilder parent) {
+ parent.then(adapter.literal("help").requires(this::canExecute)
+ .executes(this::execute));
+ }
+
+ @Override
+ public int execute(@NotNull CommandContext ctx) throws CommandSyntaxException {
+ S source = ctx.getSource();
+ SenderContext sender = adapter.senderContext(source);
+ sender.sendMessage(buildHelpMessage(source));
+ return Command.SINGLE_SUCCESS;
+ }
+
+ @Override
+ public boolean canExecute(@NotNull S source) {
+ return adapter.hasPermission(source, NickPermission.NICK_HELP);
+ }
+
+ @NotNull
+ private Component buildHelpMessage(@NotNull S source) {
+ ConfigHandler config = ConfigHandler.getInstance();
+ Component help = line(LocaleMessage.HELP_HEADER);
+
+ if (adapter.isPlayer(source)) {
+ boolean permNotRequired = !config.isNickRequiresPermission();
+
+ if (permNotRequired || adapter.hasPermission(source, NickPermission.NICK_SET)) {
+ help = help.appendNewline().append(line(LocaleMessage.HELP_SET));
+ help = help.appendNewline().append(line(LocaleMessage.HELP_RESET));
+ }
+
+ if (permNotRequired || adapter.hasPermission(source, NickPermission.NICK_SAVE)) {
+ help = help.appendNewline().append(line(LocaleMessage.HELP_SAVE));
+ help = help.appendNewline().append(line(LocaleMessage.HELP_DELETE));
+ }
+ }
+
+ if (!config.isWhoRequiresPermission() || adapter.hasPermission(source, NickPermission.NICK_WHO)) {
+ help = help.appendNewline().append(line(LocaleMessage.HELP_WHO));
+ }
+
+ if (adapter.hasPermission(source, NickPermission.NICK_ADMIN_SET)) {
+ help = help.appendNewline().append(line(LocaleMessage.HELP_ADMIN_SET));
+ }
+ if (adapter.hasPermission(source, NickPermission.NICK_ADMIN_RESET)) {
+ help = help.appendNewline().append(line(LocaleMessage.HELP_ADMIN_RESET));
+ }
+ if (adapter.hasPermission(source, NickPermission.NICK_ADMIN_DELETE)) {
+ help = help.appendNewline().append(line(LocaleMessage.HELP_ADMIN_DELETE));
+ }
+ if (adapter.hasPermission(source, NickPermission.NICK_ADMIN_LOOKUP)) {
+ help = help.appendNewline().append(line(LocaleMessage.HELP_ADMIN_LOOKUP));
+ }
+ if (adapter.hasPermission(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());
+ }
+}
diff --git a/core/src/main/java/simplexity/simplenicks/commands/subcommands/basic/ReloadSubCommand.java b/core/src/main/java/simplexity/simplenicks/commands/subcommands/basic/ReloadSubCommand.java
new file mode 100644
index 0000000..72e98f6
--- /dev/null
+++ b/core/src/main/java/simplexity/simplenicks/commands/subcommands/basic/ReloadSubCommand.java
@@ -0,0 +1,46 @@
+package simplexity.simplenicks.commands.subcommands.basic;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.commands.subcommands.SubCommand;
+import simplexity.simplenicks.config.ConfigHandler;
+import simplexity.simplenicks.config.LocaleMessage;
+import simplexity.simplenicks.logic.NickUtils;
+import simplexity.simplenicks.platform.BrigadierAdapter;
+import simplexity.simplenicks.saving.SqlHandler;
+import simplexity.simplenicks.util.NickPermission;
+
+public class ReloadSubCommand implements SubCommand {
+
+ private final BrigadierAdapter adapter;
+
+ public ReloadSubCommand(@NotNull BrigadierAdapter adapter) {
+ this.adapter = adapter;
+ }
+
+ @Override
+ public void subcommandTo(@NotNull LiteralArgumentBuilder parent) {
+ parent.then(adapter.literal("reload").requires(this::canExecute)
+ .executes(this::execute));
+ }
+
+ @Override
+ public int execute(@NotNull CommandContext ctx) throws CommandSyntaxException {
+ ConfigHandler.getInstance().reloadConfig();
+ SqlHandler.getInstance().closeDatabase();
+ SqlHandler.getInstance().init();
+ SimpleNicksCore.get().platform().getOnlinePlayers()
+ .forEach(NickUtils::refreshDisplayName);
+ sendFeedback(adapter.senderContext(ctx.getSource()), LocaleMessage.CONFIG_RELOADED, null);
+ return Command.SINGLE_SUCCESS;
+ }
+
+ @Override
+ public boolean canExecute(@NotNull S source) {
+ return adapter.hasPermission(source, NickPermission.NICK_RELOAD);
+ }
+}
diff --git a/core/src/main/java/simplexity/simplenicks/commands/subcommands/basic/ResetSubCommand.java b/core/src/main/java/simplexity/simplenicks/commands/subcommands/basic/ResetSubCommand.java
new file mode 100644
index 0000000..eb36958
--- /dev/null
+++ b/core/src/main/java/simplexity/simplenicks/commands/subcommands/basic/ResetSubCommand.java
@@ -0,0 +1,56 @@
+package simplexity.simplenicks.commands.subcommands.basic;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.commands.NicknameProcessor;
+import simplexity.simplenicks.commands.subcommands.SubCommand;
+import simplexity.simplenicks.config.LocaleMessage;
+import simplexity.simplenicks.logic.NickUtils;
+import simplexity.simplenicks.platform.BrigadierAdapter;
+import simplexity.simplenicks.platform.SenderContext;
+import simplexity.simplenicks.util.NickPermission;
+
+import java.util.UUID;
+
+public class ResetSubCommand implements SubCommand {
+
+ private final BrigadierAdapter adapter;
+
+ public ResetSubCommand(@NotNull BrigadierAdapter adapter) {
+ this.adapter = adapter;
+ }
+
+ @Override
+ public void subcommandTo(@NotNull LiteralArgumentBuilder parent) {
+ parent.then(adapter.literal("reset").requires(this::canExecute)
+ .executes(this::execute));
+ }
+
+ @Override
+ public int execute(@NotNull CommandContext ctx) {
+ S source = ctx.getSource();
+ UUID uuid = adapter.getPlayerUuid(source).orElseThrow();
+ SenderContext sender = adapter.senderContext(source);
+ SimpleNicksCore.get().platform().runAsync(() -> {
+ boolean success = NicknameProcessor.getInstance().resetNickname(uuid);
+ if (success) {
+ SimpleNicksCore.get().platform().runSync(() -> {
+ NickUtils.refreshDisplayName(uuid);
+ sendFeedback(sender, LocaleMessage.RESET_SELF, null);
+ });
+ } else {
+ sendFeedback(sender, LocaleMessage.ERROR_RESET_FAILURE, null);
+ }
+ });
+ return Command.SINGLE_SUCCESS;
+ }
+
+ @Override
+ public boolean canExecute(@NotNull S source) {
+ return adapter.isPlayer(source)
+ && (permissionNotRequired() || adapter.hasPermission(source, NickPermission.NICK_SET));
+ }
+}
diff --git a/core/src/main/java/simplexity/simplenicks/commands/subcommands/basic/SaveSubCommand.java b/core/src/main/java/simplexity/simplenicks/commands/subcommands/basic/SaveSubCommand.java
new file mode 100644
index 0000000..a631ef7
--- /dev/null
+++ b/core/src/main/java/simplexity/simplenicks/commands/subcommands/basic/SaveSubCommand.java
@@ -0,0 +1,95 @@
+package simplexity.simplenicks.commands.subcommands.basic;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.commands.NicknameProcessor;
+import simplexity.simplenicks.commands.subcommands.Exceptions;
+import simplexity.simplenicks.commands.subcommands.NickSuggestionProviders;
+import simplexity.simplenicks.commands.subcommands.SubCommand;
+import simplexity.simplenicks.config.ConfigHandler;
+import simplexity.simplenicks.config.LocaleMessage;
+import simplexity.simplenicks.logic.NickUtils;
+import simplexity.simplenicks.platform.BrigadierAdapter;
+import simplexity.simplenicks.platform.SenderContext;
+import simplexity.simplenicks.saving.Nickname;
+import simplexity.simplenicks.util.NickPermission;
+
+import java.util.UUID;
+
+public class SaveSubCommand implements SubCommand {
+
+ private final BrigadierAdapter adapter;
+
+ public SaveSubCommand(@NotNull BrigadierAdapter adapter) {
+ this.adapter = adapter;
+ }
+
+ @Override
+ public void subcommandTo(@NotNull LiteralArgumentBuilder parent) {
+ parent.then(adapter.literal("save").requires(this::canExecute)
+ .executes(this::execute)
+ .then(adapter.nicknameArgument("nickname",
+ NickSuggestionProviders.ownSavedNicks(adapter),
+ this::executeWithArgument)));
+ }
+
+ @Override
+ public int execute(@NotNull CommandContext ctx) throws CommandSyntaxException {
+ S source = ctx.getSource();
+ UUID uuid = adapter.getPlayerUuid(source).orElseThrow();
+ String username = adapter.getPlayerUsername(source).orElse(uuid.toString());
+ SenderContext sender = adapter.senderContext(source);
+ Nickname nickname = NicknameProcessor.getInstance().getCurrentNickname(uuid, true);
+ if (nickname == null) throw Exceptions.cannotSave(adapter);
+ checkSaveSlots(uuid);
+ SimpleNicksCore.get().platform().runAsync(() -> {
+ boolean saved = NicknameProcessor.getInstance().saveNickname(uuid, username, nickname.getNickname());
+ if (saved) {
+ SimpleNicksCore.get().platform().runSync(() -> {
+ NickUtils.refreshDisplayName(uuid);
+ sendFeedback(sender, LocaleMessage.SAVE_NICK, nickname);
+ });
+ } else {
+ sendFeedback(sender, LocaleMessage.ERROR_SAVE_FAILURE, nickname);
+ }
+ });
+ return Command.SINGLE_SUCCESS;
+ }
+
+ public int executeWithArgument(@NotNull CommandContext ctx) throws CommandSyntaxException {
+ S source = ctx.getSource();
+ UUID uuid = adapter.getPlayerUuid(source).orElseThrow();
+ String username = adapter.getPlayerUsername(source).orElse(uuid.toString());
+ SenderContext sender = adapter.senderContext(source);
+ Nickname nickname = adapter.getNickname(ctx, "nickname");
+ NickUtils.nicknameChecks(adapter, sender, nickname);
+ checkSaveSlots(uuid);
+ if (NicknameProcessor.getInstance().playerAlreadySavedThis(uuid, nickname.getNickname())) {
+ throw Exceptions.alreadySaved(adapter);
+ }
+ SimpleNicksCore.get().platform().runAsync(() -> {
+ boolean saved = NicknameProcessor.getInstance().saveNickname(uuid, username, nickname.getNickname());
+ if (saved) {
+ sendFeedback(sender, LocaleMessage.SAVE_NICK, nickname);
+ } else {
+ sendFeedback(sender, LocaleMessage.ERROR_SAVE_FAILURE, nickname);
+ }
+ });
+ return Command.SINGLE_SUCCESS;
+ }
+
+ @Override
+ public boolean canExecute(@NotNull S source) {
+ return adapter.isPlayer(source)
+ && (permissionNotRequired() || adapter.hasPermission(source, NickPermission.NICK_SAVE));
+ }
+
+ private void checkSaveSlots(@NotNull UUID uuid) throws CommandSyntaxException {
+ int currentUsed = NicknameProcessor.getInstance().getCurrentSavedNickCount(uuid, true);
+ if (currentUsed >= ConfigHandler.getInstance().getMaxSaves()) throw Exceptions.tooManySavedNames(adapter);
+ }
+}
diff --git a/core/src/main/java/simplexity/simplenicks/commands/subcommands/basic/SetSubCommand.java b/core/src/main/java/simplexity/simplenicks/commands/subcommands/basic/SetSubCommand.java
new file mode 100644
index 0000000..b4e7140
--- /dev/null
+++ b/core/src/main/java/simplexity/simplenicks/commands/subcommands/basic/SetSubCommand.java
@@ -0,0 +1,64 @@
+package simplexity.simplenicks.commands.subcommands.basic;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.commands.NicknameProcessor;
+import simplexity.simplenicks.commands.subcommands.Exceptions;
+import simplexity.simplenicks.commands.subcommands.SubCommand;
+import simplexity.simplenicks.config.LocaleMessage;
+import simplexity.simplenicks.logic.NickUtils;
+import simplexity.simplenicks.platform.BrigadierAdapter;
+import simplexity.simplenicks.platform.SenderContext;
+import simplexity.simplenicks.saving.Nickname;
+import simplexity.simplenicks.util.NickPermission;
+
+import java.util.UUID;
+
+public class SetSubCommand implements SubCommand {
+
+ private final BrigadierAdapter adapter;
+
+ public SetSubCommand(@NotNull BrigadierAdapter adapter) {
+ this.adapter = adapter;
+ }
+
+ @Override
+ public void subcommandTo(@NotNull LiteralArgumentBuilder parent) {
+ parent.then(adapter.literal("set").requires(this::canExecute)
+ .then(adapter.nicknameArgument("nickname", this::execute)));
+ }
+
+ @Override
+ public int execute(@NotNull CommandContext ctx) throws CommandSyntaxException {
+ S source = ctx.getSource();
+ UUID uuid = adapter.getPlayerUuid(source).orElseThrow(() -> Exceptions.invalidPlayerSpecified(adapter, ""));
+ String username = adapter.getPlayerUsername(source).orElse(uuid.toString());
+ Nickname nickname = adapter.getNickname(ctx, "nickname");
+ if (nickname.getNormalizedNickname().isEmpty()) throw Exceptions.nickIsNull(adapter);
+ SenderContext sender = adapter.senderContext(source);
+ if (!NickUtils.isValidTags(sender, nickname.getNickname())) throw Exceptions.tagsNotPermitted(adapter);
+ NickUtils.nicknameChecks(adapter, sender, nickname);
+ SimpleNicksCore.get().platform().runAsync(() -> {
+ boolean succeeded = NicknameProcessor.getInstance().setNickname(uuid, username, nickname.getNickname());
+ if (succeeded) {
+ SimpleNicksCore.get().platform().runSync(() -> {
+ refreshName(uuid, true);
+ sendFeedback(sender, LocaleMessage.SET_SELF, nickname);
+ });
+ } else {
+ sendFeedback(sender, LocaleMessage.ERROR_SET_FAILURE, nickname);
+ }
+ });
+ return Command.SINGLE_SUCCESS;
+ }
+
+ @Override
+ public boolean canExecute(@NotNull S source) {
+ return adapter.isPlayer(source)
+ && (permissionNotRequired() || adapter.hasPermission(source, NickPermission.NICK_SET));
+ }
+}
diff --git a/core/src/main/java/simplexity/simplenicks/commands/subcommands/basic/WhoSubCommand.java b/core/src/main/java/simplexity/simplenicks/commands/subcommands/basic/WhoSubCommand.java
new file mode 100644
index 0000000..f3fd0aa
--- /dev/null
+++ b/core/src/main/java/simplexity/simplenicks/commands/subcommands/basic/WhoSubCommand.java
@@ -0,0 +1,86 @@
+package simplexity.simplenicks.commands.subcommands.basic;
+
+import com.mojang.brigadier.Command;
+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 org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.commands.subcommands.NickSuggestionProviders;
+import simplexity.simplenicks.commands.subcommands.SubCommand;
+import simplexity.simplenicks.config.ConfigHandler;
+import simplexity.simplenicks.config.LocaleMessage;
+import simplexity.simplenicks.config.MessageUtils;
+import simplexity.simplenicks.logic.NickUtils;
+import simplexity.simplenicks.platform.BrigadierAdapter;
+import simplexity.simplenicks.platform.PlayerInfo;
+import simplexity.simplenicks.platform.SenderContext;
+import simplexity.simplenicks.saving.Nickname;
+import simplexity.simplenicks.util.NickPermission;
+
+import java.util.List;
+
+public class WhoSubCommand implements SubCommand {
+
+ private final BrigadierAdapter adapter;
+
+ public WhoSubCommand(@NotNull BrigadierAdapter adapter) {
+ this.adapter = adapter;
+ }
+
+ @Override
+ public void subcommandTo(@NotNull LiteralArgumentBuilder parent) {
+ parent.then(adapter.literal("who").requires(this::canExecute)
+ .then(adapter.nicknameArgument("nickname",
+ NickSuggestionProviders.activeOnlineNicks(),
+ this::execute)));
+ }
+
+ @Override
+ public int execute(@NotNull CommandContext ctx) throws CommandSyntaxException {
+ S source = ctx.getSource();
+ SenderContext sender = adapter.senderContext(source);
+ Nickname nickname = adapter.getNickname(ctx, "nickname");
+ SimpleNicksCore.get().platform().runAsync(() -> {
+ List playersWithNick = NickUtils.getPlayersByNickname(nickname.getNormalizedNickname());
+ if (playersWithNick.isEmpty()) {
+ sendFeedback(sender, LocaleMessage.ERROR_NO_PLAYERS_WITH_THIS_NAME, null);
+ return;
+ }
+ sender.sendMessage(buildWhoMessage(nickname, playersWithNick));
+ });
+ return Command.SINGLE_SUCCESS;
+ }
+
+ @Override
+ public boolean canExecute(@NotNull S source) {
+ return !ConfigHandler.getInstance().isWhoRequiresPermission()
+ || adapter.hasPermission(source, NickPermission.NICK_WHO);
+ }
+
+ @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 lastSeen = player.lastLoginMillis();
+ long timeDiffSeconds = lastSeen > 0 ? (now - lastSeen) / 1000 : 0;
+ result = result.append(SimpleNicksCore.get().miniMessage().deserialize(
+ LocaleMessage.WHO_INFO.getMessage(),
+ Placeholder.parsed("name", player.username()),
+ MessageUtils.getTimeFormat(timeDiffSeconds)));
+ }
+ return result;
+ }
+}
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 50%
rename from src/main/java/simplexity/simplenicks/logic/NickUtils.java
rename to core/src/main/java/simplexity/simplenicks/logic/NickUtils.java
index c0513c6..76b0715 100644
--- a/src/main/java/simplexity/simplenicks/logic/NickUtils.java
+++ b/core/src/main/java/simplexity/simplenicks/logic/NickUtils.java
@@ -4,14 +4,13 @@
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.BrigadierAdapter;
+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;
@@ -35,47 +34,49 @@
@SuppressWarnings("UnusedReturnValue")
public class NickUtils {
- private static final MiniMessage miniMessage = SimpleNicks.getMiniMessage();
-
+ private static MiniMessage miniMessage() {
+ 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.
+ * Performs all configured checks on a nickname using a platform adapter to render exception
+ * messages as styled text. Prefer this overload in generic subcommand code.
*
- * @param sender The sender attempting to set the nickname
- * @param nickname The nickname to validate
+ * @param adapter the platform brigadier adapter (for styled error messages)
+ * @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 BrigadierAdapter adapter,
+ @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(adapter);
}
- 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 (!bypassLength) {
- if (normalizedNick.length() > ConfigHandler.getInstance().getMaxLength())
- throw Exceptions.ERROR_LENGTH.create(normalizedNick);
+ if (!bypassUsername && ConfigHandler.getInstance().isUsernameProtection() && isProtectedUsername(normalizedNick)) {
+ throw Exceptions.nicknameSomeonesUsername(adapter, normalizedNick);
}
- if (!bypassRegex) {
- if (!passesRegexCheck(normalizedNick)) throw Exceptions.ERROR_REGEX.create(normalizedNick);
+ if (!bypassLength && normalizedNick.length() > ConfigHandler.getInstance().getMaxLength()) {
+ throw Exceptions.lengthError(adapter, normalizedNick);
}
- if (ConfigHandler.getInstance().shouldOnlineNicksBeProtected() && !bypassNickProtection) {
- if (someoneOnlineUsingThis(sender, normalizedNick))
- throw Exceptions.ERROR_SOMEONE_USING_THAT_NICKNAME.create(normalizedNick);
+ if (!bypassRegex && !passesRegexCheck(normalizedNick)) {
+ throw Exceptions.regexError(adapter, 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(adapter, normalizedNick);
+ }
+ if (ConfigHandler.getInstance().shouldOfflineNicksBeProtected() && someoneSavedUsingThis(sender, normalizedNick)) {
+ throw Exceptions.someoneUsingThatNickname(adapter, normalizedNick);
+ }
}
}
@@ -83,57 +84,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 = miniMessage().deserialize(ConfigHandler.getInstance().getNickPrefix())
+ .append(miniMessage().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, miniMessage().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 = miniMessage().deserialize(nick);
+ String defaultSerialized = miniMessage().serialize(defaultParsed);
Component permissionParsed = parser.deserialize(nick);
- String permissionSerialized = miniMessage.serialize(permissionParsed);
+ String permissionSerialized = miniMessage().serialize(permissionParsed);
return defaultSerialized.equals(permissionSerialized);
}
@@ -142,37 +143,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 miniMessage().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 +191,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 +206,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 +219,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/BrigadierAdapter.java b/core/src/main/java/simplexity/simplenicks/platform/BrigadierAdapter.java
new file mode 100644
index 0000000..118cc23
--- /dev/null
+++ b/core/src/main/java/simplexity/simplenicks/platform/BrigadierAdapter.java
@@ -0,0 +1,204 @@
+package simplexity.simplenicks.platform;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.Message;
+import com.mojang.brigadier.builder.ArgumentBuilder;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import com.mojang.brigadier.suggestion.SuggestionProvider;
+import net.kyori.adventure.text.Component;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.saving.Nickname;
+import simplexity.simplenicks.util.NickPermission;
+
+import java.util.Optional;
+import java.util.UUID;
+
+/**
+ * Abstracts all platform-specific Brigadier operations used by core command logic.
+ *
+ * One instance per platform is shared across all subcommands.
+ * Each method receives the source or context it needs rather than storing it as a field.
+ *
+ *
+ * @param the platform's CommandSourceStack type
+ */
+public interface BrigadierAdapter {
+
+ // -------------------------------------------------------------------------
+ // Source extraction
+ // -------------------------------------------------------------------------
+
+ /**
+ * Returns the UUID of the player executing the command, or empty if the source is not a player.
+ *
+ * @param source the command source
+ * @return player UUID, or empty for console/command-block sources
+ */
+ @NotNull
+ Optional getPlayerUuid(@NotNull S source);
+
+ /**
+ * Returns the username of the player executing the command, or empty if not a player.
+ *
+ * @param source the command source
+ * @return player username, or empty for non-player sources
+ */
+ @NotNull
+ Optional getPlayerUsername(@NotNull S source);
+
+ /**
+ * Returns {@code true} if the source represents an in-game player.
+ *
+ * @param source the command source
+ * @return {@code true} if the source is a player
+ */
+ boolean isPlayer(@NotNull S source);
+
+ // -------------------------------------------------------------------------
+ // Delegation
+ // -------------------------------------------------------------------------
+
+ /**
+ * Wraps the source in a {@link SenderContext} for use by core logic (messaging, permissions).
+ *
+ * @param source the command source
+ * @return a platform-backed sender context
+ */
+ @NotNull
+ SenderContext senderContext(@NotNull S source);
+
+ /**
+ * Returns {@code true} if the source holds the given permission.
+ *
+ * @param source the command source
+ * @param permission the permission to check
+ * @return {@code true} if the permission is granted
+ */
+ boolean hasPermission(@NotNull S source, @NotNull NickPermission permission);
+
+ // -------------------------------------------------------------------------
+ // Argument parsing
+ // -------------------------------------------------------------------------
+
+ /**
+ * Extracts a {@link Nickname} from the named argument in the context.
+ *
+ * Paper: the custom argument type already produced a {@code Nickname}.
+ * Fabric: parses a raw string into a {@code Nickname} on the fly.
+ *
+ *
+ * @param ctx the command context
+ * @param argName the argument name
+ * @return the resolved nickname
+ */
+ @NotNull
+ Nickname getNickname(@NotNull CommandContext ctx, @NotNull String argName);
+
+ /**
+ * Resolves a {@link PlayerTarget} from the named offline-player argument.
+ *
+ * @param ctx the command context
+ * @param argName the argument name
+ * @return the resolved player target, or empty if the player could not be found
+ * @throws CommandSyntaxException if the argument value is syntactically invalid
+ */
+ @NotNull
+ Optional getPlayerTarget(@NotNull CommandContext ctx, @NotNull String argName)
+ throws CommandSyntaxException;
+
+ // -------------------------------------------------------------------------
+ // Brigadier message
+ // -------------------------------------------------------------------------
+
+ /**
+ * Converts an Adventure {@link Component} to a Brigadier {@link Message} that the platform
+ * renders with full MiniMessage formatting when displaying a command syntax exception.
+ *
+ * @param component the component to wrap
+ * @return a Brigadier message
+ */
+ @NotNull
+ Message brigadierMessage(@NotNull Component component);
+
+ // -------------------------------------------------------------------------
+ // Literal builder
+ // -------------------------------------------------------------------------
+
+ /**
+ * Creates a Brigadier literal argument builder for the given name.
+ * Abstracts over {@code io.papermc.paper.command.brigadier.Commands.literal()} (Paper)
+ * and {@code net.minecraft.commands.Commands.literal()} (Fabric).
+ *
+ * @param name the literal string
+ * @return a new literal argument builder
+ */
+ @NotNull
+ LiteralArgumentBuilder literal(@NotNull String name);
+
+ // -------------------------------------------------------------------------
+ // Argument builder factories
+ // -------------------------------------------------------------------------
+ // Each method accepts the executor and returns a fully-configured node ready
+ // for parent.then(). This sidesteps the ArgumentBuilder wildcard-chaining
+ // problem that would arise if executors were attached by the caller.
+
+ /**
+ * Returns a nickname argument node with own-nickname suggestions and the given executor.
+ *
+ * @param argName the argument name
+ * @param executor the command executor to attach
+ * @return a configured argument builder
+ */
+ @NotNull
+ ArgumentBuilder nicknameArgument(@NotNull String argName, @NotNull Command executor);
+
+ /**
+ * Returns a nickname argument node with the given suggestion provider and executor.
+ *
+ * @param argName the argument name
+ * @param suggestions the suggestion provider
+ * @param executor the command executor to attach
+ * @return a configured argument builder
+ */
+ @NotNull
+ ArgumentBuilder nicknameArgument(@NotNull String argName,
+ @NotNull SuggestionProvider suggestions, @NotNull Command executor);
+
+ /**
+ * Returns an offline-player argument node with online-player suggestions and the given executor.
+ *
+ * @param argName the argument name
+ * @param executor the command executor to attach
+ * @return a configured argument builder
+ */
+ @NotNull
+ ArgumentBuilder playerArgument(@NotNull String argName, @NotNull Command executor);
+
+ /**
+ * Returns a two-level tree: {@code } → {@code } with the given executor on
+ * the nickname node. Used by admin subcommands that take both a player and a nickname.
+ *
+ * @param playerArgName the player argument name
+ * @param nicknameArgName the nickname argument name
+ * @param executor the command executor to attach to the nickname node
+ * @return the player argument builder with the nickname node chained
+ */
+ @NotNull
+ ArgumentBuilder playerThenNicknameArgument(@NotNull String playerArgName,
+ @NotNull String nicknameArgName, @NotNull Command executor);
+
+ /**
+ * Returns a two-level tree: {@code } → {@code } where nickname suggestions
+ * are the target player's saved nicknames. Used by admin delete.
+ *
+ * @param playerArgName the player argument name
+ * @param nicknameArgName the nickname argument name
+ * @param executor the command executor to attach to the nickname node
+ * @return the player argument builder with the nickname node chained
+ */
+ @NotNull
+ ArgumentBuilder playerThenTargetNicknameArgument(@NotNull String playerArgName,
+ @NotNull String nicknameArgName, @NotNull Command executor);
+}
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..ccfc8f9
--- /dev/null
+++ b/core/src/main/java/simplexity/simplenicks/platform/PlatformAdapter.java
@@ -0,0 +1,149 @@
+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);
+
+ /**
+ * Sends an Adventure component message to an online player.
+ * Does nothing if the player is not online.
+ *
+ * @param uuid the player's UUID
+ * @param message the component to send
+ */
+ void sendMessageToPlayer(@NotNull UUID uuid, @NotNull Component message);
+
+ /**
+ * 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/PlayerTarget.java b/core/src/main/java/simplexity/simplenicks/platform/PlayerTarget.java
new file mode 100644
index 0000000..e1f27ef
--- /dev/null
+++ b/core/src/main/java/simplexity/simplenicks/platform/PlayerTarget.java
@@ -0,0 +1,13 @@
+package simplexity.simplenicks.platform;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+/**
+ * Platform-agnostic representation of a resolved offline or online player target.
+ *
+ * @param id the player's UUID
+ * @param name the player's last-known username
+ */
+public record PlayerTarget(@NotNull UUID id, @NotNull String name) {}
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/FabricBrigadierAdapter.java b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/FabricBrigadierAdapter.java
new file mode 100644
index 0000000..9cf4c3b
--- /dev/null
+++ b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/FabricBrigadierAdapter.java
@@ -0,0 +1,186 @@
+package simplexity.simplenicks.fabric.commands;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.Message;
+import com.mojang.brigadier.arguments.StringArgumentType;
+import com.mojang.brigadier.builder.ArgumentBuilder;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import com.mojang.brigadier.suggestion.SuggestionProvider;
+import net.kyori.adventure.text.Component;
+import net.minecraft.commands.CommandSourceStack;
+import net.minecraft.commands.Commands;
+import net.minecraft.commands.arguments.GameProfileArgument;
+import net.minecraft.server.players.NameAndId;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.SimpleNicksCore;
+import simplexity.simplenicks.commands.NicknameProcessor;
+import simplexity.simplenicks.commands.subcommands.NickSuggestionProviders;
+import simplexity.simplenicks.fabric.platform.FabricPlatformAdapter;
+import simplexity.simplenicks.fabric.platform.FabricSenderContext;
+import simplexity.simplenicks.fabric.util.FabricPermissions;
+import simplexity.simplenicks.logic.NickUtils;
+import simplexity.simplenicks.platform.BrigadierAdapter;
+import simplexity.simplenicks.platform.PlayerTarget;
+import simplexity.simplenicks.platform.SenderContext;
+import simplexity.simplenicks.saving.Nickname;
+import simplexity.simplenicks.util.NickPermission;
+
+import java.util.Collection;
+import java.util.Optional;
+import java.util.UUID;
+
+/**
+ * {@link BrigadierAdapter} implementation for the Fabric platform.
+ *
+ * The command tree is built (via {@code CommandRegistrationCallback}) before {@link SimpleNicksCore} is initialized,
+ * so audiences are resolved lazily on use rather than captured at construction time.
+ *
+ */
+public class FabricBrigadierAdapter implements BrigadierAdapter {
+
+ @Override
+ public @NotNull Optional getPlayerUuid(@NotNull CommandSourceStack source) {
+ return source.getPlayer() != null
+ ? Optional.of(source.getPlayer().getUUID())
+ : Optional.empty();
+ }
+
+ @Override
+ public @NotNull Optional getPlayerUsername(@NotNull CommandSourceStack source) {
+ return source.getPlayer() != null
+ ? Optional.of(source.getPlayer().getGameProfile().name())
+ : Optional.empty();
+ }
+
+ @Override
+ public boolean isPlayer(@NotNull CommandSourceStack source) {
+ return source.getPlayer() != null;
+ }
+
+ @Override
+ public @NotNull SenderContext senderContext(@NotNull CommandSourceStack source) {
+ return new FabricSenderContext(source);
+ }
+
+ @Override
+ public boolean hasPermission(@NotNull CommandSourceStack source, @NotNull NickPermission permission) {
+ return FabricPermissions.check(source, permission);
+ }
+
+ @Override
+ public @NotNull Nickname getNickname(@NotNull CommandContext ctx, @NotNull String argName) {
+ String rawNickname = StringArgumentType.getString(ctx, argName);
+ return new Nickname(rawNickname, NickUtils.normalizeNickname(rawNickname));
+ }
+
+ @Override
+ public @NotNull Optional getPlayerTarget(
+ @NotNull CommandContext ctx, @NotNull String argName)
+ throws CommandSyntaxException {
+ Collection profiles = GameProfileArgument.getGameProfiles(ctx, argName);
+ return profiles.stream()
+ .findFirst()
+ .map(profile -> new PlayerTarget(profile.id(), profile.name()));
+ }
+
+ @Override
+ public @NotNull Message brigadierMessage(@NotNull Component component) {
+ FabricPlatformAdapter platform = (FabricPlatformAdapter) SimpleNicksCore.get().platform();
+ return platform.getAudiences().asNative(component);
+ }
+
+ @Override
+ public @NotNull LiteralArgumentBuilder literal(@NotNull String name) {
+ return Commands.literal(name);
+ }
+
+ @Override
+ public @NotNull ArgumentBuilder nicknameArgument(
+ @NotNull String argName, @NotNull Command executor) {
+ return Commands.argument(argName, StringArgumentType.greedyString())
+ .suggests(NickSuggestionProviders.ownSavedNicks(this))
+ .executes(executor);
+ }
+
+ @Override
+ public @NotNull ArgumentBuilder nicknameArgument(
+ @NotNull String argName,
+ @NotNull SuggestionProvider suggestions,
+ @NotNull Command executor) {
+ return Commands.argument(argName, StringArgumentType.greedyString())
+ .suggests(suggestions)
+ .executes(executor);
+ }
+
+ @Override
+ public @NotNull ArgumentBuilder playerArgument(
+ @NotNull String argName, @NotNull Command executor) {
+ return Commands.argument(argName, GameProfileArgument.gameProfile())
+ .suggests((ctx, builder) -> {
+ ctx.getSource().getServer().getPlayerList().getPlayers()
+ .forEach(player -> builder.suggest(player.getGameProfile().name()));
+ return builder.buildFuture();
+ })
+ .executes(executor);
+ }
+
+ @Override
+ public @NotNull ArgumentBuilder playerThenNicknameArgument(
+ @NotNull String playerArgName, @NotNull String nicknameArgName,
+ @NotNull Command executor) {
+ return Commands.argument(playerArgName, GameProfileArgument.gameProfile())
+ .suggests((ctx, builder) -> {
+ ctx.getSource().getServer().getPlayerList().getPlayers()
+ .forEach(player -> builder.suggest(player.getGameProfile().name()));
+ return builder.buildFuture();
+ })
+ .then(Commands.argument(nicknameArgName, StringArgumentType.greedyString())
+ .suggests((ctx, builder) -> {
+ try {
+ GameProfileArgument.getGameProfiles(ctx, playerArgName).stream()
+ .findFirst()
+ .ifPresent(profile -> {
+ boolean online = ctx.getSource().getServer()
+ .getPlayerList().getPlayer(profile.id()) != null;
+ NicknameProcessor.getInstance()
+ .getSavedNicknames(profile.id(), online)
+ .forEach(savedNickname -> builder.suggest(savedNickname.getNickname()));
+ });
+ } catch (CommandSyntaxException ignored) {
+ }
+ return builder.buildFuture();
+ })
+ .executes(executor));
+ }
+
+ @Override
+ public @NotNull ArgumentBuilder playerThenTargetNicknameArgument(
+ @NotNull String playerArgName, @NotNull String nicknameArgName,
+ @NotNull Command executor) {
+ return Commands.argument(playerArgName, GameProfileArgument.gameProfile())
+ .suggests((ctx, builder) -> {
+ ctx.getSource().getServer().getPlayerList().getPlayers()
+ .forEach(player -> builder.suggest(player.getGameProfile().name()));
+ return builder.buildFuture();
+ })
+ .then(Commands.argument(nicknameArgName, StringArgumentType.greedyString())
+ .suggests((ctx, builder) -> {
+ try {
+ GameProfileArgument.getGameProfiles(ctx, playerArgName).stream()
+ .findFirst()
+ .ifPresent(profile -> {
+ boolean online = ctx.getSource().getServer()
+ .getPlayerList().getPlayer(profile.id()) != null;
+ NicknameProcessor.getInstance()
+ .getSavedNicknames(profile.id(), online)
+ .forEach(savedNickname -> builder.suggest(savedNickname.getNickname()));
+ });
+ } catch (CommandSyntaxException ignored) {
+ }
+ return builder.buildFuture();
+ })
+ .executes(executor));
+ }
+}
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..e55a210
--- /dev/null
+++ b/fabric/src/main/java/simplexity/simplenicks/fabric/commands/FabricNicknameCommand.java
@@ -0,0 +1,42 @@
+package simplexity.simplenicks.fabric.commands;
+
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import net.minecraft.commands.CommandSourceStack;
+import net.minecraft.commands.Commands;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.commands.subcommands.admin.AdminSubCommand;
+import simplexity.simplenicks.commands.subcommands.basic.DeleteSubCommand;
+import simplexity.simplenicks.commands.subcommands.basic.HelpSubCommand;
+import simplexity.simplenicks.commands.subcommands.basic.ReloadSubCommand;
+import simplexity.simplenicks.commands.subcommands.basic.ResetSubCommand;
+import simplexity.simplenicks.commands.subcommands.basic.SaveSubCommand;
+import simplexity.simplenicks.commands.subcommands.basic.SetSubCommand;
+import simplexity.simplenicks.commands.subcommands.basic.WhoSubCommand;
+import simplexity.simplenicks.config.ConfigHandler;
+import simplexity.simplenicks.fabric.util.FabricPermissions;
+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() {
+ FabricBrigadierAdapter adapter = new FabricBrigadierAdapter();
+ LiteralArgumentBuilder builder = Commands.literal("nick")
+ .requires(src -> !ConfigHandler.getInstance().isNickRequiresPermission()
+ || FabricPermissions.check(src, NickPermission.NICK_COMMAND));
+ new HelpSubCommand<>(adapter).subcommandTo(builder);
+ new SetSubCommand<>(adapter).subcommandTo(builder);
+ new SaveSubCommand<>(adapter).subcommandTo(builder);
+ new ResetSubCommand<>(adapter).subcommandTo(builder);
+ new DeleteSubCommand<>(adapter).subcommandTo(builder);
+ new AdminSubCommand<>(adapter).subcommandTo(builder);
+ new ReloadSubCommand<>(adapter).subcommandTo(builder);
+ new WhoSubCommand<>(adapter).subcommandTo(builder);
+ return builder;
+ }
+}
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..f3769a4
--- /dev/null
+++ b/fabric/src/main/java/simplexity/simplenicks/fabric/platform/FabricPlatformAdapter.java
@@ -0,0 +1,207 @@
+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((_, 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 void sendMessageToPlayer(@NotNull UUID uuid, @NotNull Component message) {
+ ServerPlayer player = server.getPlayerList().getPlayer(uuid);
+ if (player == null) return;
+ player.sendSystemMessage(audiences.asNative(message));
+ }
+
+ @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..ef30446
--- /dev/null
+++ b/fabric/src/main/java/simplexity/simplenicks/fabric/platform/FabricSenderContext.java
@@ -0,0 +1,63 @@
+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.util.FabricPermissions;
+import simplexity.simplenicks.platform.SenderContext;
+import simplexity.simplenicks.saving.Cache;
+import simplexity.simplenicks.saving.Nickname;
+
+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.
+ *
+ */
+public class FabricSenderContext implements SenderContext {
+
+ private final CommandSourceStack source;
+
+ public FabricSenderContext(@NotNull CommandSourceStack source) {
+ this.source = source;
+ }
+
+ @Override
+ public boolean hasPermission(@NotNull String permission) {
+ return FabricPermissions.check(source, permission);
+ }
+
+ @Override
+ public @NotNull Optional getUuid() {
+ ServerPlayer player = source.getPlayer();
+ if (player == null) return Optional.empty();
+ return Optional.of(player.getUUID());
+ }
+
+ @Override
+ public void sendMessage(@NotNull Component message) {
+ FabricPlatformAdapter adapter = (FabricPlatformAdapter) SimpleNicksCore.get().platform();
+ net.minecraft.network.chat.Component nms = adapter.getAudiences().asNative(message);
+ source.sendSuccess(() -> nms, false);
+ }
+
+ @Override
+ public boolean isPlayer() {
+ return source.getPlayer() != null;
+ }
+
+ @Override
+ public @NotNull String getDisplayName() {
+ ServerPlayer player = source.getPlayer();
+ if (player == null) return "[Console]";
+ Nickname nick = Cache.getInstance().getActiveNickname(player.getUUID());
+ return nick != null ? nick.getNickname() : player.getGameProfile().name();
+ }
+}
diff --git a/fabric/src/main/java/simplexity/simplenicks/fabric/storage/FabricNicknameStorage.java b/fabric/src/main/java/simplexity/simplenicks/fabric/storage/FabricNicknameStorage.java
new file mode 100644
index 0000000..3149a32
--- /dev/null
+++ b/fabric/src/main/java/simplexity/simplenicks/fabric/storage/FabricNicknameStorage.java
@@ -0,0 +1,61 @@
+package simplexity.simplenicks.fabric.storage;
+
+import net.minecraft.network.chat.Component;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Static storage for per-player display name components on the Fabric platform.
+ *
+ * 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
+ *
+ */
+public final class FabricPermissions {
+
+ private FabricPermissions() {
+ }
+
+ public static boolean check(@NotNull CommandSourceStack source, @NotNull NickPermission permission) {
+ return switch (permission.getPermissionDefault()) {
+ case "true" -> Permissions.check(source, permission.getPermissionKey(), true);
+ case "false" -> Permissions.check(source, permission.getPermissionKey(), false);
+ default -> Permissions.check(source, permission.getPermissionKey(), 2);
+ };
+ }
+
+ public static boolean check(@NotNull ServerPlayer player, @NotNull NickPermission permission) {
+ return switch (permission.getPermissionDefault()) {
+ case "true" -> Permissions.check(player, permission.getPermissionKey(), true);
+ case "false" -> Permissions.check(player, permission.getPermissionKey(), false);
+ default -> Permissions.check(player, permission.getPermissionKey(), 2);
+ };
+ }
+
+ /**
+ * String-key variants used by platform adapters and sender contexts. Looks up the matching
+ * {@link NickPermission} to resolve the correct fallback; defaults to op-level-2 if the key
+ * is not a registered permission.
+ */
+ public static boolean check(@NotNull CommandSourceStack source, @NotNull String permissionKey) {
+ for (NickPermission perm : NickPermission.values()) {
+ if (perm.getPermissionKey().equals(permissionKey)) {
+ return check(source, perm);
+ }
+ }
+ return Permissions.check(source, permissionKey, 2);
+ }
+
+ public static boolean check(@NotNull ServerPlayer player, @NotNull String permissionKey) {
+ for (NickPermission perm : NickPermission.values()) {
+ if (perm.getPermissionKey().equals(permissionKey)) {
+ return check(player, perm);
+ }
+ }
+ return Permissions.check(player, permissionKey, 2);
+ }
+}
diff --git a/src/main/resources/config.yml b/fabric/src/main/resources/config.yml
similarity index 100%
rename from src/main/resources/config.yml
rename to fabric/src/main/resources/config.yml
diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json
new file mode 100644
index 0000000..6e10f42
--- /dev/null
+++ b/fabric/src/main/resources/fabric.mod.json
@@ -0,0 +1,21 @@
+{
+ "schemaVersion": 1,
+ "id": "simplenicks",
+ "version": "3.2.2",
+ "name": "SimpleNicks",
+ "description": "A simple mod to allow players to set and reset nicknames.",
+ "authors": ["Rhythmic", "Peashooter101"],
+ "license": "AGPL-3.0",
+ "environment": "server",
+ "entrypoints": {
+ "main": ["simplexity.simplenicks.fabric.SimpleNicksFabric"]
+ },
+ "mixins": [
+ "simplenicks.mixins.json"
+ ],
+ "depends": {
+ "fabricloader": ">=0.16.0",
+ "fabric-api": "*",
+ "minecraft": "~26.1"
+ }
+}
diff --git a/fabric/src/main/resources/simplenicks.mixins.json b/fabric/src/main/resources/simplenicks.mixins.json
new file mode 100644
index 0000000..7c729e7
--- /dev/null
+++ b/fabric/src/main/resources/simplenicks.mixins.json
@@ -0,0 +1,13 @@
+{
+ "required": true,
+ "package": "simplexity.simplenicks.fabric.mixin",
+ "compatibilityLevel": "JAVA_25",
+ "mixins": [
+ "PlayerMixin",
+ "ServerLoginPacketListenerAccessor",
+ "ServerPlayerMixin"
+ ],
+ "injectors": {
+ "defaultRequire": 1
+ }
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..7a0f487
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,11 @@
+adventureVersion=4.17.0
+miniMessageVersion=4.17.0
+hikariVersion=6.3.0
+slf4jVersion=2.0.16
+paperVersion=1.21.5-R0.1-SNAPSHOT
+fabricMinecraftVersion=26.1
+fabricLoaderVersion=0.19.2
+fabricApiVersion=0.145.1+26.1
+adventureFabricVersion=6.9.0
+fabricMiniMessageVersion=4.26.1
+fabricPermissionsVersion=0.7.0
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f8e1ee3
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..dbc3ce4
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..adff685
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,248 @@
+#!/bin/sh
+
+#
+# Copyright © 2015 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..e509b2d
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,93 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/paper/build.gradle b/paper/build.gradle
new file mode 100644
index 0000000..d207ff9
--- /dev/null
+++ b/paper/build.gradle
@@ -0,0 +1,37 @@
+plugins {
+ id 'io.papermc.paperweight.userdev' version '2.0.0-beta.17'
+ id 'com.gradleup.shadow' version '9.0.0-beta4'
+}
+
+dependencies {
+ paperweight.paperDevBundle("${paperVersion}")
+ implementation project(':core')
+ compileOnly "me.clip:placeholderapi:2.11.5"
+ compileOnly "io.github.miniplaceholders:miniplaceholders-api:3.2.0"
+}
+
+shadowJar {
+ relocate 'com.zaxxer.hikari', 'simplexity.simplenicks.shaded.hikari'
+ manifest {
+ attributes 'paperweight-mappings-namespace': 'mojang'
+ }
+}
+
+tasks.withType(JavaCompile).configureEach {
+ options.release = 21
+}
+
+// options.release = 21 causes Gradle to request JVM 21-compatible deps,
+// which conflicts with miniplaceholders-api:3.2.0 (requires JVM 25).
+// Override the attribute to request JVM 25 deps while still producing Java 21 bytecode.
+configurations.named('compileClasspath') {
+ attributes {
+ attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 25)
+ }
+}
+
+tasks.named('reobfJar') {
+ outputJar = layout.buildDirectory.file("libs/SimpleNicks-paper-${project.version}.jar")
+}
+
+assemble.dependsOn reobfJar
diff --git a/src/main/java/simplexity/simplenicks/SimpleNicks.java b/paper/src/main/java/simplexity/simplenicks/SimpleNicks.java
similarity index 54%
rename from src/main/java/simplexity/simplenicks/SimpleNicks.java
rename to paper/src/main/java/simplexity/simplenicks/SimpleNicks.java
index 6925c02..614c7c7 100644
--- a/src/main/java/simplexity/simplenicks/SimpleNicks.java
+++ b/paper/src/main/java/simplexity/simplenicks/SimpleNicks.java
@@ -1,100 +1,84 @@
package simplexity.simplenicks;
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
-import net.kyori.adventure.text.minimessage.MiniMessage;
-import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
-import org.bukkit.plugin.Plugin;
+import org.bukkit.permissions.Permission;
+import org.bukkit.permissions.PermissionDefault;
import org.bukkit.plugin.java.JavaPlugin;
-import simplexity.simplenicks.commands.NicknameCommand;
+import simplexity.simplenicks.commands.PaperNicknameCommand;
import simplexity.simplenicks.config.ConfigHandler;
import simplexity.simplenicks.hooks.SNExpansion;
import simplexity.simplenicks.hooks.SNMiniExpansion;
import simplexity.simplenicks.listener.LeaveListener;
import simplexity.simplenicks.listener.LoginListener;
+import simplexity.simplenicks.platform.PaperPlatformAdapter;
import simplexity.simplenicks.saving.SaveMigrator;
import simplexity.simplenicks.saving.SqlHandler;
import simplexity.simplenicks.util.ColorTag;
import simplexity.simplenicks.util.FormatTag;
import simplexity.simplenicks.util.NickPermission;
-import java.util.logging.Logger;
-
-
@SuppressWarnings("UnstableApiUsage")
public final class SimpleNicks extends JavaPlugin {
- private static MiniMessage miniMessage;
- private static Plugin instance;
-
@Override
public void onEnable() {
- instance = this;
this.saveDefaultConfig();
getConfig().options().copyDefaults(true);
saveConfig();
+
+ PaperPlatformAdapter adapter = new PaperPlatformAdapter(this);
+ SimpleNicksCore.initialize(adapter);
+
+ ConfigHandler.getInstance().reloadConfig();
+ SqlHandler.getInstance().init();
+ SaveMigrator.migrateFromYml();
+
if (this.getServer().getPluginManager().isPluginEnabled("PlaceholderAPI")) {
new SNExpansion().register();
}
if (this.getServer().getPluginManager().isPluginEnabled("MiniPlaceholders")) {
SNMiniExpansion.register();
}
+
getServer().getPluginManager().registerEvents(new LoginListener(), this);
getServer().getPluginManager().registerEvents(new LeaveListener(), this);
- configReload();
- SqlHandler.getInstance().init();
- SaveMigrator.migrateFromYml();
+
this.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, commands -> {
- commands.registrar().register(NicknameCommand.createCommand().build());
+ commands.registrar().register(PaperNicknameCommand.createCommand().build());
});
- setUpMiniMessage();
+
registerPermissions();
}
+ @Override
+ public void onDisable() {
+ SqlHandler.getInstance().closeDatabase();
+ SimpleNicksCore.shutdown();
+ }
+
private void registerPermissions() {
for (NickPermission perm : NickPermission.values()) {
- getServer().getPluginManager().addPermission(perm.getPermission());
+ getServer().getPluginManager().addPermission(
+ new Permission(perm.getPermissionKey(), toDefault(perm.getPermissionDefault()))
+ );
}
- for (ColorTag perm : ColorTag.values()) {
- getServer().getPluginManager().addPermission(perm.getPermission());
+ for (ColorTag tag : ColorTag.values()) {
+ getServer().getPluginManager().addPermission(
+ new Permission(tag.getPermissionKey(), toDefault(tag.getPermissionDefault()))
+ );
}
- }
-
- private static void setUpMiniMessage() {
- TagResolver.Builder tagResolver = TagResolver.builder();
- for (ColorTag colorTag : ColorTag.values()) {
- tagResolver.resolver(colorTag.getTagResolver());
- }
- for (FormatTag formatTag : FormatTag.values()) {
- tagResolver.resolver(formatTag.getTagResolver());
+ for (FormatTag tag : FormatTag.values()) {
+ getServer().getPluginManager().addPermission(
+ new Permission(tag.getPermissionKey(), toDefault(tag.getPermissionDefault()))
+ );
}
- TagResolver resolver = tagResolver.build();
- miniMessage = MiniMessage.builder()
- .strict(false)
- .tags(resolver)
- .build();
- }
-
- public static MiniMessage getMiniMessage() {
- return miniMessage;
- }
-
- public static Plugin getInstance() {
- return instance;
- }
-
- public static Logger getSimpleNicksLogger() {
- return instance.getLogger();
}
-
- public static void configReload() {
- ConfigHandler.getInstance().reloadConfig();
+ private static PermissionDefault toDefault(String value) {
+ return switch (value.toLowerCase()) {
+ case "true" -> PermissionDefault.TRUE;
+ case "false" -> PermissionDefault.FALSE;
+ default -> PermissionDefault.OP;
+ };
}
-
- @Override
- public void onDisable() {
- SqlHandler.getInstance().closeDatabase();
- }
-
-
}
diff --git a/src/main/java/simplexity/simplenicks/commands/NicknameCommand.java b/paper/src/main/java/simplexity/simplenicks/commands/PaperNicknameCommand.java
similarity index 58%
rename from src/main/java/simplexity/simplenicks/commands/NicknameCommand.java
rename to paper/src/main/java/simplexity/simplenicks/commands/PaperNicknameCommand.java
index 77910fc..9566b33 100644
--- a/src/main/java/simplexity/simplenicks/commands/NicknameCommand.java
+++ b/paper/src/main/java/simplexity/simplenicks/commands/PaperNicknameCommand.java
@@ -3,34 +3,36 @@
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import io.papermc.paper.command.brigadier.CommandSourceStack;
import io.papermc.paper.command.brigadier.Commands;
+import org.jetbrains.annotations.NotNull;
import simplexity.simplenicks.commands.subcommands.admin.AdminSubCommand;
import simplexity.simplenicks.commands.subcommands.basic.DeleteSubCommand;
import simplexity.simplenicks.commands.subcommands.basic.HelpSubCommand;
-import simplexity.simplenicks.commands.subcommands.basic.WhoSubCommand;
import simplexity.simplenicks.commands.subcommands.basic.ReloadSubCommand;
import simplexity.simplenicks.commands.subcommands.basic.ResetSubCommand;
import simplexity.simplenicks.commands.subcommands.basic.SaveSubCommand;
import simplexity.simplenicks.commands.subcommands.basic.SetSubCommand;
+import simplexity.simplenicks.commands.subcommands.basic.WhoSubCommand;
import simplexity.simplenicks.config.ConfigHandler;
+import simplexity.simplenicks.platform.PaperBrigadierAdapter;
import simplexity.simplenicks.util.NickPermission;
@SuppressWarnings("UnstableApiUsage")
-public class NicknameCommand {
+public class PaperNicknameCommand {
+ @NotNull
public static LiteralArgumentBuilder createCommand() {
+ PaperBrigadierAdapter adapter = new PaperBrigadierAdapter();
LiteralArgumentBuilder builder = Commands.literal("nick")
- .requires(src -> !ConfigHandler.getInstance().isNickRequiresPermission() || src.getSender().hasPermission(NickPermission.NICK_COMMAND.getPermission()));
- new HelpSubCommand().subcommandTo(builder);
- new SetSubCommand().subcommandTo(builder);
- new SaveSubCommand().subcommandTo(builder);
- new ResetSubCommand().subcommandTo(builder);
- new DeleteSubCommand().subcommandTo(builder);
- new AdminSubCommand().subcommandTo(builder);
- new ReloadSubCommand().subcommandTo(builder);
- new WhoSubCommand().subcommandTo(builder);
+ .requires(src -> !ConfigHandler.getInstance().isNickRequiresPermission()
+ || src.getSender().hasPermission(NickPermission.NICK_COMMAND.getPermissionKey()));
+ new HelpSubCommand<>(adapter).subcommandTo(builder);
+ new SetSubCommand<>(adapter).subcommandTo(builder);
+ new SaveSubCommand<>(adapter).subcommandTo(builder);
+ new ResetSubCommand<>(adapter).subcommandTo(builder);
+ new DeleteSubCommand<>(adapter).subcommandTo(builder);
+ new AdminSubCommand<>(adapter).subcommandTo(builder);
+ new ReloadSubCommand<>(adapter).subcommandTo(builder);
+ new WhoSubCommand<>(adapter).subcommandTo(builder);
return builder;
}
-
-
-
}
diff --git a/src/main/java/simplexity/simplenicks/commands/arguments/NicknameArgument.java b/paper/src/main/java/simplexity/simplenicks/commands/arguments/NicknameArgument.java
similarity index 61%
rename from src/main/java/simplexity/simplenicks/commands/arguments/NicknameArgument.java
rename to paper/src/main/java/simplexity/simplenicks/commands/arguments/NicknameArgument.java
index 4ee2a7c..6f2092f 100644
--- a/src/main/java/simplexity/simplenicks/commands/arguments/NicknameArgument.java
+++ b/paper/src/main/java/simplexity/simplenicks/commands/arguments/NicknameArgument.java
@@ -13,7 +13,7 @@
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
-import simplexity.simplenicks.SimpleNicks;
+import simplexity.simplenicks.SimpleNicksCore;
import simplexity.simplenicks.commands.NicknameProcessor;
import simplexity.simplenicks.saving.Cache;
import simplexity.simplenicks.saving.Nickname;
@@ -31,44 +31,18 @@
@SuppressWarnings("UnstableApiUsage")
public class NicknameArgument implements CustomArgumentType {
-
- /**
- * Parses a nickname string from the command input into a {@link Nickname} object.
- * The normalized nickname is generated by stripping MiniMessage tags and converting
- * to lowercase.
- *
- * @param reader The Brigadier StringReader containing the command input
- * @return A {@link Nickname} object representing the parsed nickname
- * @throws CommandSyntaxException if parsing fails
- */
@Override
public @NotNull Nickname parse(@NotNull StringReader reader) throws CommandSyntaxException {
String nickname = getNativeType().parse(reader);
- String normalizedNickname = SimpleNicks.getMiniMessage().stripTags(nickname).toLowerCase();
+ String normalizedNickname = SimpleNicksCore.get().miniMessage().stripTags(nickname).toLowerCase();
return new Nickname(nickname, normalizedNickname);
}
- /**
- * Gets the native Brigadier type for this argument.
- * Uses a greedy string to allow spaces in nicknames.
- *
- * @return The native {@link ArgumentType} for nicknames
- */
@Override
public @NotNull ArgumentType getNativeType() {
return StringArgumentType.greedyString();
}
-
- /**
- * Provides suggestions for nicknames saved by the command sender.
- *
- * @param context Command context
- * @param builder SuggestionsBuilder for adding completions
- * @param Typically {@link io.papermc.paper.command.brigadier.CommandSourceStack}
- * @return A CompletableFuture containing the suggestions
- */
-
public @NotNull CompletableFuture suggestOwnNicknames(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) {
CommandSourceStack css = (CommandSourceStack) context.getSource();
OfflinePlayer player = (OfflinePlayer) css.getSender();
@@ -76,15 +50,6 @@ public class NicknameArgument implements CustomArgumentType {
return builder.buildFuture();
}
-
- /**
- * Provides suggestions for nicknames saved by another player argument.
- *
- * @param context Command context
- * @param builder SuggestionsBuilder for adding completions
- * @param Typically {@link io.papermc.paper.command.brigadier.CommandSourceStack}
- * @return A CompletableFuture containing the suggestions
- */
public @NotNull CompletableFuture suggestOtherNicknames(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) {
OfflinePlayer player = context.getArgument("player", OfflinePlayer.class);
if (player == null) return builder.buildFuture();
@@ -92,16 +57,6 @@ public class NicknameArgument implements CustomArgumentType {
return builder.buildFuture();
}
-
-
- /**
- * Provides combined suggestions for nicknames of both the command sender and another player.
- *
- * @param context Command context
- * @param builder SuggestionsBuilder for adding completions
- * @param Typically {@link io.papermc.paper.command.brigadier.CommandSourceStack}
- * @return A CompletableFuture containing the suggestions
- */
public @NotNull CompletableFuture suggestOwnAndOtherNicknames(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) {
OfflinePlayer player = context.getArgument("player", OfflinePlayer.class);
CommandSourceStack css = (CommandSourceStack) context.getSource();
@@ -111,15 +66,7 @@ public class NicknameArgument implements CustomArgumentType {
return builder.buildFuture();
}
- /**
- * Provides suggestions for all online nicknames.
- *
- * @param context Command context
- * @param builder SuggestionsBuilder for adding completions
- * @param Typically {@link io.papermc.paper.command.brigadier.CommandSourceStack}
- * @return A CompletableFuture containing the suggestions
- */
- public @NotNull CompletableFuture suggestAllOnlineNicknames(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder){
+ public @NotNull CompletableFuture suggestAllOnlineNicknames(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) {
for (Nickname nickname : Cache.getInstance().getOnlineNicknames().values()) {
builder.suggest(nickname.getNormalizedNickname());
}
@@ -127,20 +74,18 @@ public class NicknameArgument implements CustomArgumentType {
}
private void addSuggestionsForPlayer(@NotNull SuggestionsBuilder builder, @NotNull OfflinePlayer player) {
- for (Nickname nickname : NicknameProcessor.getInstance().getSavedNicknames(player)) {
+ for (Nickname nickname : NicknameProcessor.getInstance().getSavedNicknames(player.getUniqueId(), player.isOnline())) {
String suggestion = nickname.getNickname();
String suggestionStripped = nickname.getNormalizedNickname();
- if (suggestionStripped.toLowerCase().contains(builder.getRemainingLowerCase()) || suggestion.toLowerCase().contains(builder.getRemainingLowerCase())) {
+ if (suggestionStripped.toLowerCase().contains(builder.getRemainingLowerCase())
+ || suggestion.toLowerCase().contains(builder.getRemainingLowerCase())) {
builder.suggest(
suggestion,
MessageComponentSerializer.message().serialize(
- SimpleNicks.getMiniMessage().deserialize("Preview: " + nickname.getNickname())
+ SimpleNicksCore.get().miniMessage().deserialize("Preview: " + nickname.getNickname())
)
);
}
}
}
-
-
-
}
diff --git a/src/main/java/simplexity/simplenicks/commands/arguments/OfflinePlayerArgument.java b/paper/src/main/java/simplexity/simplenicks/commands/arguments/OfflinePlayerArgument.java
similarity index 68%
rename from src/main/java/simplexity/simplenicks/commands/arguments/OfflinePlayerArgument.java
rename to paper/src/main/java/simplexity/simplenicks/commands/arguments/OfflinePlayerArgument.java
index c28e9ef..d98beb9 100644
--- a/src/main/java/simplexity/simplenicks/commands/arguments/OfflinePlayerArgument.java
+++ b/paper/src/main/java/simplexity/simplenicks/commands/arguments/OfflinePlayerArgument.java
@@ -7,15 +7,17 @@
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
+import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
import io.papermc.paper.command.brigadier.MessageComponentSerializer;
import io.papermc.paper.command.brigadier.argument.CustomArgumentType;
+import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
-import simplexity.simplenicks.SimpleNicks;
+import simplexity.simplenicks.SimpleNicksCore;
import simplexity.simplenicks.commands.NicknameProcessor;
-import simplexity.simplenicks.commands.subcommands.Exceptions;
+import simplexity.simplenicks.config.LocaleMessage;
import simplexity.simplenicks.saving.Nickname;
import java.util.concurrent.CompletableFuture;
@@ -31,13 +33,6 @@
@SuppressWarnings("UnstableApiUsage")
public class OfflinePlayerArgument implements CustomArgumentType {
- /**
- * Parses a player name from the command input into an {@link OfflinePlayer} object.
- *
- * @param reader The Brigadier StringReader containing the command input
- * @return The {@link OfflinePlayer} corresponding to the input
- * @throws CommandSyntaxException if the player does not exist or has never joined
- */
@Override
public @NotNull OfflinePlayer parse(@NotNull StringReader reader) throws CommandSyntaxException {
String playerName = reader.readString();
@@ -48,39 +43,28 @@ public class OfflinePlayerArgument implements CustomArgumentType getNativeType() {
return StringArgumentType.word();
}
- /**
- * Provides suggestions for online players.
- * Each suggestion includes a hoverable preview of their current nickname.
- *
- * @param ignoredContext Command context (unused)
- * @param builder SuggestionsBuilder for adding completions
- * @param Typically {@link io.papermc.paper.command.brigadier.CommandSourceStack}
- * @return A CompletableFuture containing the suggestions
- */
public @NotNull CompletableFuture suggestOnlinePlayers(@NotNull CommandContext ignoredContext, @NotNull SuggestionsBuilder builder) {
for (Player player : Bukkit.getOnlinePlayers()) {
String suggestion = player.getName();
if (suggestion.toLowerCase().contains(builder.getRemainingLowerCase())) {
- Nickname currentNick = NicknameProcessor.getInstance().getCurrentNickname(player);
+ Nickname currentNick = NicknameProcessor.getInstance().getCurrentNickname(player.getUniqueId(), true);
String nickDisplay = currentNick != null ? currentNick.getNickname() : player.getName();
builder.suggest(
suggestion,
MessageComponentSerializer.message().serialize(
- SimpleNicks.getMiniMessage().deserialize("Current Nickname: " + nickDisplay)
+ SimpleNicksCore.get().miniMessage().deserialize("Current Nickname: " + nickDisplay)
)
);
}
diff --git a/src/main/java/simplexity/simplenicks/hooks/SNExpansion.java b/paper/src/main/java/simplexity/simplenicks/hooks/SNExpansion.java
similarity index 65%
rename from src/main/java/simplexity/simplenicks/hooks/SNExpansion.java
rename to paper/src/main/java/simplexity/simplenicks/hooks/SNExpansion.java
index 8ff7f36..472c672 100644
--- a/src/main/java/simplexity/simplenicks/hooks/SNExpansion.java
+++ b/paper/src/main/java/simplexity/simplenicks/hooks/SNExpansion.java
@@ -4,12 +4,13 @@
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.bukkit.OfflinePlayer;
import org.jetbrains.annotations.NotNull;
-import simplexity.simplenicks.SimpleNicks;
+import simplexity.simplenicks.SimpleNicksCore;
import simplexity.simplenicks.config.ConfigHandler;
import simplexity.simplenicks.saving.Cache;
import simplexity.simplenicks.saving.Nickname;
public class SNExpansion extends PlaceholderExpansion {
+
@Override
public @NotNull String getIdentifier() {
return "simplenick";
@@ -41,26 +42,14 @@ public String onRequest(@NotNull OfflinePlayer player, @NotNull String params) {
} else {
nickname = nick.getNickname();
}
- if (params.equalsIgnoreCase("mininick")) {
- return nickname;
- }
- if (params.equalsIgnoreCase("prefixed-mininick")) {
- return prefix + nickname;
- }
- if (params.equalsIgnoreCase("stripped")) {
- return SimpleNicks.getMiniMessage().stripTags(nickname);
- }
- if (params.equalsIgnoreCase("prefixed-stripped")) {
- return prefix + SimpleNicks.getMiniMessage().stripTags(nickname);
- }
+ if (params.equalsIgnoreCase("mininick")) return nickname;
+ if (params.equalsIgnoreCase("prefixed-mininick")) return prefix + nickname;
+ if (params.equalsIgnoreCase("stripped")) return SimpleNicksCore.get().miniMessage().stripTags(nickname);
+ if (params.equalsIgnoreCase("prefixed-stripped")) return prefix + SimpleNicksCore.get().miniMessage().stripTags(nickname);
String parsedNickname = LegacyComponentSerializer.legacySection()
- .serialize(SimpleNicks.getMiniMessage().deserialize(nickname));
- if (params.equalsIgnoreCase("nickname")) {
- return parsedNickname;
- }
- if (params.equalsIgnoreCase("prefixed-nickname")) {
- return prefix + parsedNickname;
- }
+ .serialize(SimpleNicksCore.get().miniMessage().deserialize(nickname));
+ if (params.equalsIgnoreCase("nickname")) return parsedNickname;
+ if (params.equalsIgnoreCase("prefixed-nickname")) return prefix + parsedNickname;
if (params.equalsIgnoreCase("normalized")) {
if (nick == null) return null;
return nick.getNormalizedNickname();
diff --git a/src/main/java/simplexity/simplenicks/hooks/SNMiniExpansion.java b/paper/src/main/java/simplexity/simplenicks/hooks/SNMiniExpansion.java
similarity index 68%
rename from src/main/java/simplexity/simplenicks/hooks/SNMiniExpansion.java
rename to paper/src/main/java/simplexity/simplenicks/hooks/SNMiniExpansion.java
index a35718d..8003991 100644
--- a/src/main/java/simplexity/simplenicks/hooks/SNMiniExpansion.java
+++ b/paper/src/main/java/simplexity/simplenicks/hooks/SNMiniExpansion.java
@@ -4,7 +4,7 @@
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.tag.Tag;
import org.bukkit.entity.Player;
-import simplexity.simplenicks.SimpleNicks;
+import simplexity.simplenicks.SimpleNicksCore;
import simplexity.simplenicks.config.ConfigHandler;
import simplexity.simplenicks.saving.Cache;
import simplexity.simplenicks.saving.Nickname;
@@ -13,44 +13,33 @@ public class SNMiniExpansion {
/**
* Builds and registers the MiniPlaceholders expansion for SimpleNicks.
- *
- * Registers the following audience-scoped tags (online players only):
- *
- * - {@code } — rendered nickname component (MiniMessage-parsed)
- * - {@code } — config prefix + rendered nickname
- * - {@code } — nickname with all MiniMessage tags removed
- * - {@code } — prefix + stripped nickname
- * - {@code } — normalized (lowercase, stripped) nickname
- *
- * Falls back to the player's username when no nickname is set.
- *
*/
public static void register() {
Expansion.builder("simplenick")
.audiencePlaceholder(Player.class, "nick", (player, queue, ctx) -> {
Nickname nick = Cache.getInstance().getActiveNickname(player.getUniqueId());
String rawNick = nick != null ? nick.getNickname() : player.getName();
- return Tag.inserting(SimpleNicks.getMiniMessage().deserialize(rawNick));
+ return Tag.inserting(SimpleNicksCore.get().miniMessage().deserialize(rawNick));
})
.audiencePlaceholder(Player.class, "prefixed_nick", (player, queue, ctx) -> {
Nickname nick = Cache.getInstance().getActiveNickname(player.getUniqueId());
String rawNick = nick != null ? nick.getNickname() : player.getName();
String prefix = ConfigHandler.getInstance().getNickPrefix();
return Tag.inserting(
- Component.text(prefix).append(SimpleNicks.getMiniMessage().deserialize(rawNick))
+ Component.text(prefix).append(SimpleNicksCore.get().miniMessage().deserialize(rawNick))
);
})
.audiencePlaceholder(Player.class, "stripped", (player, queue, ctx) -> {
Nickname nick = Cache.getInstance().getActiveNickname(player.getUniqueId());
String rawNick = nick != null ? nick.getNickname() : player.getName();
- return Tag.inserting(Component.text(SimpleNicks.getMiniMessage().stripTags(rawNick)));
+ return Tag.inserting(Component.text(SimpleNicksCore.get().miniMessage().stripTags(rawNick)));
})
.audiencePlaceholder(Player.class, "prefixed_stripped", (player, queue, ctx) -> {
Nickname nick = Cache.getInstance().getActiveNickname(player.getUniqueId());
String rawNick = nick != null ? nick.getNickname() : player.getName();
String prefix = ConfigHandler.getInstance().getNickPrefix();
return Tag.inserting(
- Component.text(prefix + SimpleNicks.getMiniMessage().stripTags(rawNick))
+ Component.text(prefix + SimpleNicksCore.get().miniMessage().stripTags(rawNick))
);
})
.audiencePlaceholder(Player.class, "normalized", (player, queue, ctx) -> {
diff --git a/src/main/java/simplexity/simplenicks/listener/LeaveListener.java b/paper/src/main/java/simplexity/simplenicks/listener/LeaveListener.java
similarity index 100%
rename from src/main/java/simplexity/simplenicks/listener/LeaveListener.java
rename to paper/src/main/java/simplexity/simplenicks/listener/LeaveListener.java
diff --git a/src/main/java/simplexity/simplenicks/listener/LoginListener.java b/paper/src/main/java/simplexity/simplenicks/listener/LoginListener.java
similarity index 82%
rename from src/main/java/simplexity/simplenicks/listener/LoginListener.java
rename to paper/src/main/java/simplexity/simplenicks/listener/LoginListener.java
index e5f4281..05bd112 100644
--- a/src/main/java/simplexity/simplenicks/listener/LoginListener.java
+++ b/paper/src/main/java/simplexity/simplenicks/listener/LoginListener.java
@@ -1,28 +1,28 @@
package simplexity.simplenicks.listener;
-import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
-import simplexity.simplenicks.SimpleNicks;
-import simplexity.simplenicks.saving.Cache;
+import simplexity.simplenicks.SimpleNicksCore;
import simplexity.simplenicks.logic.NickUtils;
+import simplexity.simplenicks.saving.Cache;
import simplexity.simplenicks.saving.SaveMigrator;
import simplexity.simplenicks.saving.SqlHandler;
import java.util.UUID;
public class LoginListener implements Listener {
+
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
public void onPlayerJoin(PlayerJoinEvent joinEvent) {
UUID playerUuid = joinEvent.getPlayer().getUniqueId();
String username = joinEvent.getPlayer().getName();
- Bukkit.getScheduler().runTaskAsynchronously(SimpleNicks.getInstance(), () -> {
+ SimpleNicksCore.get().platform().runAsync(() -> {
SqlHandler.getInstance().updatePlayerTable(playerUuid, username);
Cache.getInstance().loadCurrentNickname(playerUuid);
Cache.getInstance().loadSavedNicknames(playerUuid);
- Bukkit.getScheduler().runTask(SimpleNicks.getInstance(), () -> {
+ SimpleNicksCore.get().platform().runSync(() -> {
SaveMigrator.migratePdcNickname(joinEvent.getPlayer());
NickUtils.refreshDisplayName(playerUuid);
});
diff --git a/paper/src/main/java/simplexity/simplenicks/platform/BukkitConfigProvider.java b/paper/src/main/java/simplexity/simplenicks/platform/BukkitConfigProvider.java
new file mode 100644
index 0000000..cefa4ba
--- /dev/null
+++ b/paper/src/main/java/simplexity/simplenicks/platform/BukkitConfigProvider.java
@@ -0,0 +1,125 @@
+package simplexity.simplenicks.platform;
+
+import org.bukkit.configuration.InvalidConfigurationException;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * {@link ConfigProvider} backed by Bukkit's {@link FileConfiguration}.
+ *
+ * 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/PaperBrigadierAdapter.java b/paper/src/main/java/simplexity/simplenicks/platform/PaperBrigadierAdapter.java
new file mode 100644
index 0000000..6e1cb07
--- /dev/null
+++ b/paper/src/main/java/simplexity/simplenicks/platform/PaperBrigadierAdapter.java
@@ -0,0 +1,134 @@
+package simplexity.simplenicks.platform;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.Message;
+import com.mojang.brigadier.builder.ArgumentBuilder;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import com.mojang.brigadier.suggestion.SuggestionProvider;
+import io.papermc.paper.command.brigadier.CommandSourceStack;
+import io.papermc.paper.command.brigadier.Commands;
+import io.papermc.paper.command.brigadier.MessageComponentSerializer;
+import net.kyori.adventure.text.Component;
+import org.bukkit.OfflinePlayer;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import simplexity.simplenicks.commands.arguments.NicknameArgument;
+import simplexity.simplenicks.commands.arguments.OfflinePlayerArgument;
+import simplexity.simplenicks.saving.Nickname;
+import simplexity.simplenicks.util.NickPermission;
+
+import java.util.Optional;
+import java.util.UUID;
+
+/**
+ * {@link BrigadierAdapter} implementation for the Paper platform.
+ */
+@SuppressWarnings("UnstableApiUsage")
+public class PaperBrigadierAdapter implements BrigadierAdapter {
+
+ @Override
+ public @NotNull Optional getPlayerUuid(@NotNull CommandSourceStack source) {
+ return source.getSender() instanceof Player player ? Optional.of(player.getUniqueId()) : Optional.empty();
+ }
+
+ @Override
+ public @NotNull Optional getPlayerUsername(@NotNull CommandSourceStack source) {
+ return source.getSender() instanceof Player player ? Optional.of(player.getName()) : Optional.empty();
+ }
+
+ @Override
+ public boolean isPlayer(@NotNull CommandSourceStack source) {
+ return source.getSender() instanceof Player;
+ }
+
+ @Override
+ public @NotNull SenderContext senderContext(@NotNull CommandSourceStack source) {
+ return new PaperSenderContext(source.getSender());
+ }
+
+ @Override
+ public boolean hasPermission(@NotNull CommandSourceStack source, @NotNull NickPermission permission) {
+ return source.getSender().hasPermission(permission.getPermissionKey());
+ }
+
+ @Override
+ public @NotNull Nickname getNickname(@NotNull CommandContext ctx, @NotNull String argName) {
+ return ctx.getArgument(argName, Nickname.class);
+ }
+
+ @Override
+ public @NotNull Optional getPlayerTarget(
+ @NotNull CommandContext ctx, @NotNull String argName)
+ throws CommandSyntaxException {
+ OfflinePlayer player = ctx.getArgument(argName, OfflinePlayer.class);
+ if (player.getName() == null) return Optional.empty();
+ return Optional.of(new PlayerTarget(player.getUniqueId(), player.getName()));
+ }
+
+ @Override
+ public @NotNull Message brigadierMessage(@NotNull Component component) {
+ return MessageComponentSerializer.message().serialize(component);
+ }
+
+ @Override
+ public @NotNull LiteralArgumentBuilder literal(@NotNull String name) {
+ return Commands.literal(name);
+ }
+
+ @Override
+ public @NotNull ArgumentBuilder nicknameArgument(
+ @NotNull String argName, @NotNull Command executor) {
+ NicknameArgument nicknameArg = new NicknameArgument();
+ return Commands.argument(argName, nicknameArg)
+ .suggests(nicknameArg::suggestOwnNicknames)
+ .executes(executor);
+ }
+
+ @Override
+ public @NotNull ArgumentBuilder nicknameArgument(
+ @NotNull String argName,
+ @NotNull SuggestionProvider suggestions,
+ @NotNull Command executor) {
+ NicknameArgument nicknameArg = new NicknameArgument();
+ return Commands.argument(argName, nicknameArg)
+ .suggests(suggestions)
+ .executes(executor);
+ }
+
+ @Override
+ public @NotNull ArgumentBuilder playerArgument(
+ @NotNull String argName, @NotNull Command executor) {
+ OfflinePlayerArgument playerArg = new OfflinePlayerArgument();
+ return Commands.argument(argName, playerArg)
+ .suggests(playerArg::suggestOnlinePlayers)
+ .executes(executor);
+ }
+
+ @Override
+ public @NotNull ArgumentBuilder