From 0521ee688dbf9096934bb7c90175ab26688a61ec Mon Sep 17 00:00:00 2001 From: BaileyCodes Date: Sat, 30 May 2026 21:15:08 -0500 Subject: [PATCH] feat: full name injection - nicks replace real names in tab complete, /tpa, /msg, and all commands --- .../com/nickcore/paper/NickCorePlugin.java | 16 +- .../nickcore/paper/command/NickCommand.java | 26 +-- .../listener/CommandPreprocessListener.java | 69 ++++++++ .../listener/PlayerConnectionListener.java | 14 +- .../paper/listener/TabCompleteListener.java | 59 +++++++ .../paper/service/NameOverrideService.java | 155 ++++++++++++++++++ 6 files changed, 317 insertions(+), 22 deletions(-) create mode 100644 nickcore-paper/src/main/java/com/nickcore/paper/listener/CommandPreprocessListener.java create mode 100644 nickcore-paper/src/main/java/com/nickcore/paper/listener/TabCompleteListener.java create mode 100644 nickcore-paper/src/main/java/com/nickcore/paper/service/NameOverrideService.java diff --git a/nickcore-paper/src/main/java/com/nickcore/paper/NickCorePlugin.java b/nickcore-paper/src/main/java/com/nickcore/paper/NickCorePlugin.java index 4deff13..4fa8f8f 100644 --- a/nickcore-paper/src/main/java/com/nickcore/paper/NickCorePlugin.java +++ b/nickcore-paper/src/main/java/com/nickcore/paper/NickCorePlugin.java @@ -9,7 +9,9 @@ import com.nickcore.paper.command.NickCommand; import com.nickcore.paper.gui.GuiManager; import com.nickcore.paper.integration.NickCoreExpansion; import com.nickcore.paper.integration.VelocityMessaging; +import com.nickcore.paper.listener.CommandPreprocessListener; import com.nickcore.paper.listener.PlayerConnectionListener; +import com.nickcore.paper.listener.TabCompleteListener; import com.nickcore.paper.repository.DatabaseManager; import com.nickcore.paper.repository.SqlNickRepository; import com.nickcore.paper.scheduler.SchedulerAdapter; @@ -40,6 +42,7 @@ public final class NickCorePlugin extends JavaPlugin { private PlaceholderService placeholderService; private NametagService nametagService; private ChatService chatService; + private NameOverrideService nameOverrideService; private SchedulerAdapter scheduler; private VelocityMessaging velocityMessaging; private NickCoreExpansion placeholderExpansion; @@ -87,6 +90,7 @@ public final class NickCorePlugin extends JavaPlugin { placeholderService = new PlaceholderService(profileCache, rankService); nametagService = new NametagService(profileCache, rankService, getLogger()); chatService = new ChatService(profileCache, rankService, getLogger()); + nameOverrideService = new NameOverrideService(profileCache); // Load data files loadNames(); @@ -104,8 +108,8 @@ public final class NickCorePlugin extends JavaPlugin { // Commands NickCommand nickCommand = new NickCommand(nickService, rankService, skinService, - placeholderService, nametagService, profileCache, guiManager, - getLogger(), () -> { reloadPlugin(); return null; }); + placeholderService, nametagService, nameOverrideService, profileCache, + guiManager, getLogger(), () -> { reloadPlugin(); return null; }); var cmd = getCommand("nick"); if (cmd != null) { @@ -116,9 +120,14 @@ public final class NickCorePlugin extends JavaPlugin { // Listeners Bukkit.getPluginManager().registerEvents( new PlayerConnectionListener(this, nickService, skinService, - placeholderService, nametagService, scheduler, getLogger()), this); + placeholderService, nametagService, nameOverrideService, + scheduler, getLogger()), this); Bukkit.getPluginManager().registerEvents(chatService, this); Bukkit.getPluginManager().registerEvents(guiManager, this); + Bukkit.getPluginManager().registerEvents( + new CommandPreprocessListener(nameOverrideService, getLogger()), this); + Bukkit.getPluginManager().registerEvents( + new TabCompleteListener(nameOverrideService), this); // Integrations registerPlaceholderAPI(); @@ -146,6 +155,7 @@ public final class NickCorePlugin extends JavaPlugin { } // Clear caches + if (nameOverrideService != null) nameOverrideService.clear(); if (profileCache != null) profileCache.clear(); if (placeholderService != null) placeholderService.clear(); if (skinService != null) skinService.clearCache(); diff --git a/nickcore-paper/src/main/java/com/nickcore/paper/command/NickCommand.java b/nickcore-paper/src/main/java/com/nickcore/paper/command/NickCommand.java index f83d1ff..19acbf3 100644 --- a/nickcore-paper/src/main/java/com/nickcore/paper/command/NickCommand.java +++ b/nickcore-paper/src/main/java/com/nickcore/paper/command/NickCommand.java @@ -7,6 +7,7 @@ import com.nickcore.paper.gui.GuiManager; import com.nickcore.paper.service.NickService; import com.nickcore.paper.service.PlaceholderService; import com.nickcore.paper.service.NametagService; +import com.nickcore.paper.service.NameOverrideService; import com.nickcore.paper.service.RankService; import com.nickcore.paper.service.SkinService; import com.nickcore.paper.cache.ProfileCache; @@ -49,6 +50,7 @@ public final class NickCommand implements CommandExecutor, TabCompleter { private final SkinService skinService; private final PlaceholderService placeholderService; private final NametagService nametagService; + private final NameOverrideService nameOverrideService; private final ProfileCache profileCache; private final GuiManager guiManager; private final Logger logger; @@ -57,13 +59,15 @@ public final class NickCommand implements CommandExecutor, TabCompleter { public NickCommand(NickService nickService, RankService rankService, SkinService skinService, PlaceholderService placeholderService, - NametagService nametagService, ProfileCache profileCache, - GuiManager guiManager, Logger logger, Supplier reloadAction) { + NametagService nametagService, NameOverrideService nameOverrideService, + ProfileCache profileCache, GuiManager guiManager, + Logger logger, Supplier reloadAction) { this.nickService = nickService; this.rankService = rankService; this.skinService = skinService; this.placeholderService = placeholderService; this.nametagService = nametagService; + this.nameOverrideService = nameOverrideService; this.profileCache = profileCache; this.guiManager = guiManager; this.logger = logger; @@ -226,7 +230,8 @@ public final class NickCommand implements CommandExecutor, TabCompleter { sendMessage(player, "Usage: /nick info "); return; } - Player target = Bukkit.getPlayer(args[1]); + // Resolve by nick name first, then real name + Player target = nameOverrideService.resolvePlayer(args[1]).orElse(null); if (target == null) { sendMessage(player, "Player not found."); return; @@ -253,7 +258,8 @@ public final class NickCommand implements CommandExecutor, TabCompleter { sendMessage(player, "Usage: /nick force "); return; } - Player target = Bukkit.getPlayer(args[1]); + // Resolve by nick name first, then real name + Player target = nameOverrideService.resolvePlayer(args[1]).orElse(null); if (target == null) { sendMessage(player, "Player not found."); return; @@ -280,7 +286,8 @@ public final class NickCommand implements CommandExecutor, TabCompleter { sendMessage(player, "Usage: /nick clear "); return; } - Player target = Bukkit.getPlayer(args[1]); + // Resolve by nick name first, then real name + Player target = nameOverrideService.resolvePlayer(args[1]).orElse(null); if (target == null) { sendMessage(player, "Player not found."); return; @@ -301,13 +308,8 @@ public final class NickCommand implements CommandExecutor, TabCompleter { Bukkit.getScheduler().runTask( Bukkit.getPluginManager().getPlugin("NickCore"), () -> { - NickProfile profile = profileCache.get(player.getUniqueId()).orElse(null); - if (profile != null && profile.effectiveDisplayName() != null) { - player.displayName(mm.deserialize( - InputSanitizer.escapeMiniMessage(profile.effectiveDisplayName()))); - } else { - player.displayName(Component.text(player.getName())); - } + // Apply full name override (display, player list, custom name, lookup maps) + nameOverrideService.applyFullOverride(player); nametagService.updateNametag(player); }); } diff --git a/nickcore-paper/src/main/java/com/nickcore/paper/listener/CommandPreprocessListener.java b/nickcore-paper/src/main/java/com/nickcore/paper/listener/CommandPreprocessListener.java new file mode 100644 index 0000000..d3cf15a --- /dev/null +++ b/nickcore-paper/src/main/java/com/nickcore/paper/listener/CommandPreprocessListener.java @@ -0,0 +1,69 @@ +package com.nickcore.paper.listener; + +import com.nickcore.paper.service.NameOverrideService; + +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; + +import java.util.Objects; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Intercepts player commands and replaces nick names with real names before dispatch. + * + *

This ensures that commands like /tpa, /msg, /tp, /tell, /w, /pay, /trade, + * /party, /friend, etc. work correctly when players use nicked names as arguments.

+ * + *

How it works: scans command arguments for known nick names and replaces them + * with the corresponding real player name so the target plugin can resolve them.

+ */ +public final class CommandPreprocessListener implements Listener { + + private final NameOverrideService nameOverrideService; + private final Logger logger; + + /** Pattern to split command into parts while preserving structure. */ + private static final Pattern WORD_BOUNDARY = Pattern.compile("\\s+"); + + public CommandPreprocessListener(NameOverrideService nameOverrideService, Logger logger) { + this.nameOverrideService = Objects.requireNonNull(nameOverrideService); + this.logger = Objects.requireNonNull(logger); + } + + @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) + public void onCommandPreprocess(PlayerCommandPreprocessEvent event) { + String message = event.getMessage(); + if (message.length() < 2) return; + + // Split: "/command arg1 arg2 ..." + String[] parts = message.split("\\s+"); + if (parts.length < 2) return; // No arguments to process + + boolean modified = false; + StringBuilder rebuilt = new StringBuilder(parts[0]); // Keep the command itself + + for (int i = 1; i < parts.length; i++) { + rebuilt.append(' '); + String arg = parts[i]; + + // Check if this argument is a nick name + var resolved = nameOverrideService.resolvePlayer(arg); + if (resolved.isPresent() && nameOverrideService.isNickName(arg)) { + // Replace nick with real name so the command can find the player + rebuilt.append(resolved.get().getName()); + modified = true; + } else { + rebuilt.append(arg); + } + } + + if (modified) { + event.setMessage(rebuilt.toString()); + } + } +} diff --git a/nickcore-paper/src/main/java/com/nickcore/paper/listener/PlayerConnectionListener.java b/nickcore-paper/src/main/java/com/nickcore/paper/listener/PlayerConnectionListener.java index aa5da41..36d0fb2 100644 --- a/nickcore-paper/src/main/java/com/nickcore/paper/listener/PlayerConnectionListener.java +++ b/nickcore-paper/src/main/java/com/nickcore/paper/listener/PlayerConnectionListener.java @@ -26,19 +26,21 @@ public final class PlayerConnectionListener implements Listener { private final SkinService skinService; private final PlaceholderService placeholderService; private final NametagService nametagService; + private final NameOverrideService nameOverrideService; private final SchedulerAdapter scheduler; private final Logger logger; private final MiniMessage mm = MiniMessage.miniMessage(); public PlayerConnectionListener(Plugin plugin, NickService nickService, SkinService skinService, PlaceholderService placeholderService, - NametagService nametagService, SchedulerAdapter scheduler, - Logger logger) { + NametagService nametagService, NameOverrideService nameOverrideService, + SchedulerAdapter scheduler, Logger logger) { this.plugin = plugin; this.nickService = nickService; this.skinService = skinService; this.placeholderService = placeholderService; this.nametagService = nametagService; + this.nameOverrideService = nameOverrideService; this.scheduler = scheduler; this.logger = logger; } @@ -56,11 +58,8 @@ public final class PlayerConnectionListener implements Listener { scheduler.runAtEntity(plugin, player, () -> { if (!player.isOnline()) return; - // Apply display name - if (profile.effectiveDisplayName() != null) { - player.displayName(mm.deserialize( - InputSanitizer.escapeMiniMessage(profile.effectiveDisplayName()))); - } + // Apply full name override (display name, player list, custom name, lookup maps) + nameOverrideService.applyFullOverride(player); // Apply nametag nametagService.updateNametag(player); @@ -89,6 +88,7 @@ public final class PlayerConnectionListener implements Listener { }); // Cleanup + nameOverrideService.clearMapping(player); nickService.unloadProfile(player.getUniqueId()); placeholderService.remove(player.getUniqueId()); nametagService.removeNametag(player); diff --git a/nickcore-paper/src/main/java/com/nickcore/paper/listener/TabCompleteListener.java b/nickcore-paper/src/main/java/com/nickcore/paper/listener/TabCompleteListener.java new file mode 100644 index 0000000..5fd0f39 --- /dev/null +++ b/nickcore-paper/src/main/java/com/nickcore/paper/listener/TabCompleteListener.java @@ -0,0 +1,59 @@ +package com.nickcore.paper.listener; + +import com.nickcore.paper.service.NameOverrideService; + +import com.destroystokyo.paper.event.server.AsyncTabCompleteEvent; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Replaces real player names with nick names in tab completions. + * + *

When a player presses TAB to autocomplete a command argument, this listener + * intercepts the completion results and swaps real names for nicked names. + * This means if someone types "/tpa Sha" and a player is nicked as "ShadowWolf", + * "ShadowWolf" will appear in the completions instead of their real name.

+ */ +public final class TabCompleteListener implements Listener { + + private final NameOverrideService nameOverrideService; + + public TabCompleteListener(NameOverrideService nameOverrideService) { + this.nameOverrideService = Objects.requireNonNull(nameOverrideService); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onTabComplete(AsyncTabCompleteEvent event) { + if (event.getCompletions().isEmpty()) return; + + List original = event.getCompletions(); + List replaced = new ArrayList<>(original.size()); + boolean modified = false; + + for (String completion : original) { + // Check if this completion is a real player name that has a nick + Player player = Bukkit.getPlayerExact(completion); + if (player != null) { + String display = nameOverrideService.getNickOrReal(player.getName()); + if (!display.equals(player.getName())) { + replaced.add(display); + modified = true; + continue; + } + } + replaced.add(completion); + } + + if (modified) { + event.setCompletions(replaced); + } + } +} diff --git a/nickcore-paper/src/main/java/com/nickcore/paper/service/NameOverrideService.java b/nickcore-paper/src/main/java/com/nickcore/paper/service/NameOverrideService.java new file mode 100644 index 0000000..2213fa1 --- /dev/null +++ b/nickcore-paper/src/main/java/com/nickcore/paper/service/NameOverrideService.java @@ -0,0 +1,155 @@ +package com.nickcore.paper.service; + +import com.nickcore.common.model.NickProfile; +import com.nickcore.paper.cache.ProfileCache; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Manages full name override so that nicked names replace real names everywhere: + * tab completion, /tpa, /msg, player list, nametags, and any plugin that resolves + * players by display name. + * + *

Maintains a bidirectional mapping between nick names and real names so that + * commands referencing a nick name can be resolved back to the actual player.

+ * + *

Thread-safe: all maps are ConcurrentHashMap.

+ */ +public final class NameOverrideService { + + private final ProfileCache profileCache; + private final MiniMessage miniMessage = MiniMessage.miniMessage(); + + /** nick (lowercase) -> real player UUID */ + private final ConcurrentHashMap nickToUuid = new ConcurrentHashMap<>(512); + + /** real name (lowercase) -> nick name */ + private final ConcurrentHashMap realToNick = new ConcurrentHashMap<>(512); + + public NameOverrideService(ProfileCache profileCache) { + this.profileCache = Objects.requireNonNull(profileCache); + } + + /** + * Applies a full name override to a player: display name, player list name, + * custom name, and registers the nick in lookup maps. + */ + public void applyFullOverride(Player player) { + NickProfile profile = profileCache.get(player.getUniqueId()).orElse(null); + + // Clear old mapping for this player + clearMapping(player); + + if (profile == null || !profile.nicked() || profile.nickname() == null) { + // Not nicked — restore real identity + player.displayName(Component.text(player.getName())); + player.playerListName(Component.text(player.getName())); + player.customName(null); + return; + } + + String nick = profile.nickname(); + Component nickComponent = Component.text(nick); + + // Set all name surfaces + player.displayName(nickComponent); + player.playerListName(nickComponent); + player.customName(nickComponent); + + // Register bidirectional mapping + nickToUuid.put(nick.toLowerCase(), player.getUniqueId()); + realToNick.put(player.getName().toLowerCase(), nick); + } + + /** + * Clears all name mappings for a player. Called on quit or reset. + */ + public void clearMapping(Player player) { + String realLower = player.getName().toLowerCase(); + String oldNick = realToNick.remove(realLower); + if (oldNick != null) { + nickToUuid.remove(oldNick.toLowerCase()); + } + } + + /** + * Resolves a player by either their nick name or real name. + * This is the key method that allows /tpa, /msg, etc. to work with nicks. + * + * @param name the name to look up (nick or real) + * @return the player if found and online + */ + public Optional resolvePlayer(String name) { + if (name == null) return Optional.empty(); + String lower = name.toLowerCase(); + + // 1. Check if it's a nick name + UUID uuid = nickToUuid.get(lower); + if (uuid != null) { + Player player = Bukkit.getPlayer(uuid); + if (player != null && player.isOnline()) return Optional.of(player); + } + + // 2. Fall back to real name + Player player = Bukkit.getPlayerExact(name); + return Optional.ofNullable(player); + } + + /** + * Gets the display name for a player (nick if active, real name otherwise). + */ + public String getDisplayName(Player player) { + String nick = realToNick.get(player.getName().toLowerCase()); + return nick != null ? nick : player.getName(); + } + + /** + * Gets the display name for a player by UUID. + */ + public String getDisplayName(UUID uuid) { + Player player = Bukkit.getPlayer(uuid); + if (player == null) return null; + return getDisplayName(player); + } + + /** + * Returns the nick name for a real name, or the real name if not nicked. + * Used by tab completion to replace real names with nicks. + */ + public String getNickOrReal(String realName) { + String nick = realToNick.get(realName.toLowerCase()); + return nick != null ? nick : realName; + } + + /** + * Checks if a name is a registered nick. + */ + public boolean isNickName(String name) { + return nickToUuid.containsKey(name.toLowerCase()); + } + + /** + * Returns a snapshot of all active nick-to-UUID mappings. + */ + public Map getNickMappings() { + return Map.copyOf(nickToUuid); + } + + /** + * Clears all mappings. Called on plugin shutdown. + */ + public void clear() { + nickToUuid.clear(); + realToNick.clear(); + } +}