feat: full name injection - nicks replace real names in tab complete, /tpa, /msg, and all commands

This commit is contained in:
2026-05-30 21:15:08 -05:00
parent f0b35e89ac
commit 0521ee688d
6 changed files with 317 additions and 22 deletions

View File

@@ -9,7 +9,9 @@ import com.nickcore.paper.command.NickCommand;
import com.nickcore.paper.gui.GuiManager; import com.nickcore.paper.gui.GuiManager;
import com.nickcore.paper.integration.NickCoreExpansion; import com.nickcore.paper.integration.NickCoreExpansion;
import com.nickcore.paper.integration.VelocityMessaging; import com.nickcore.paper.integration.VelocityMessaging;
import com.nickcore.paper.listener.CommandPreprocessListener;
import com.nickcore.paper.listener.PlayerConnectionListener; import com.nickcore.paper.listener.PlayerConnectionListener;
import com.nickcore.paper.listener.TabCompleteListener;
import com.nickcore.paper.repository.DatabaseManager; import com.nickcore.paper.repository.DatabaseManager;
import com.nickcore.paper.repository.SqlNickRepository; import com.nickcore.paper.repository.SqlNickRepository;
import com.nickcore.paper.scheduler.SchedulerAdapter; import com.nickcore.paper.scheduler.SchedulerAdapter;
@@ -40,6 +42,7 @@ public final class NickCorePlugin extends JavaPlugin {
private PlaceholderService placeholderService; private PlaceholderService placeholderService;
private NametagService nametagService; private NametagService nametagService;
private ChatService chatService; private ChatService chatService;
private NameOverrideService nameOverrideService;
private SchedulerAdapter scheduler; private SchedulerAdapter scheduler;
private VelocityMessaging velocityMessaging; private VelocityMessaging velocityMessaging;
private NickCoreExpansion placeholderExpansion; private NickCoreExpansion placeholderExpansion;
@@ -87,6 +90,7 @@ public final class NickCorePlugin extends JavaPlugin {
placeholderService = new PlaceholderService(profileCache, rankService); placeholderService = new PlaceholderService(profileCache, rankService);
nametagService = new NametagService(profileCache, rankService, getLogger()); nametagService = new NametagService(profileCache, rankService, getLogger());
chatService = new ChatService(profileCache, rankService, getLogger()); chatService = new ChatService(profileCache, rankService, getLogger());
nameOverrideService = new NameOverrideService(profileCache);
// Load data files // Load data files
loadNames(); loadNames();
@@ -104,8 +108,8 @@ public final class NickCorePlugin extends JavaPlugin {
// Commands // Commands
NickCommand nickCommand = new NickCommand(nickService, rankService, skinService, NickCommand nickCommand = new NickCommand(nickService, rankService, skinService,
placeholderService, nametagService, profileCache, guiManager, placeholderService, nametagService, nameOverrideService, profileCache,
getLogger(), () -> { reloadPlugin(); return null; }); guiManager, getLogger(), () -> { reloadPlugin(); return null; });
var cmd = getCommand("nick"); var cmd = getCommand("nick");
if (cmd != null) { if (cmd != null) {
@@ -116,9 +120,14 @@ public final class NickCorePlugin extends JavaPlugin {
// Listeners // Listeners
Bukkit.getPluginManager().registerEvents( Bukkit.getPluginManager().registerEvents(
new PlayerConnectionListener(this, nickService, skinService, new PlayerConnectionListener(this, nickService, skinService,
placeholderService, nametagService, scheduler, getLogger()), this); placeholderService, nametagService, nameOverrideService,
scheduler, getLogger()), this);
Bukkit.getPluginManager().registerEvents(chatService, this); Bukkit.getPluginManager().registerEvents(chatService, this);
Bukkit.getPluginManager().registerEvents(guiManager, this); Bukkit.getPluginManager().registerEvents(guiManager, this);
Bukkit.getPluginManager().registerEvents(
new CommandPreprocessListener(nameOverrideService, getLogger()), this);
Bukkit.getPluginManager().registerEvents(
new TabCompleteListener(nameOverrideService), this);
// Integrations // Integrations
registerPlaceholderAPI(); registerPlaceholderAPI();
@@ -146,6 +155,7 @@ public final class NickCorePlugin extends JavaPlugin {
} }
// Clear caches // Clear caches
if (nameOverrideService != null) nameOverrideService.clear();
if (profileCache != null) profileCache.clear(); if (profileCache != null) profileCache.clear();
if (placeholderService != null) placeholderService.clear(); if (placeholderService != null) placeholderService.clear();
if (skinService != null) skinService.clearCache(); if (skinService != null) skinService.clearCache();

View File

@@ -7,6 +7,7 @@ import com.nickcore.paper.gui.GuiManager;
import com.nickcore.paper.service.NickService; import com.nickcore.paper.service.NickService;
import com.nickcore.paper.service.PlaceholderService; import com.nickcore.paper.service.PlaceholderService;
import com.nickcore.paper.service.NametagService; import com.nickcore.paper.service.NametagService;
import com.nickcore.paper.service.NameOverrideService;
import com.nickcore.paper.service.RankService; import com.nickcore.paper.service.RankService;
import com.nickcore.paper.service.SkinService; import com.nickcore.paper.service.SkinService;
import com.nickcore.paper.cache.ProfileCache; import com.nickcore.paper.cache.ProfileCache;
@@ -49,6 +50,7 @@ public final class NickCommand implements CommandExecutor, TabCompleter {
private final SkinService skinService; private final SkinService skinService;
private final PlaceholderService placeholderService; private final PlaceholderService placeholderService;
private final NametagService nametagService; private final NametagService nametagService;
private final NameOverrideService nameOverrideService;
private final ProfileCache profileCache; private final ProfileCache profileCache;
private final GuiManager guiManager; private final GuiManager guiManager;
private final Logger logger; private final Logger logger;
@@ -57,13 +59,15 @@ public final class NickCommand implements CommandExecutor, TabCompleter {
public NickCommand(NickService nickService, RankService rankService, public NickCommand(NickService nickService, RankService rankService,
SkinService skinService, PlaceholderService placeholderService, SkinService skinService, PlaceholderService placeholderService,
NametagService nametagService, ProfileCache profileCache, NametagService nametagService, NameOverrideService nameOverrideService,
GuiManager guiManager, Logger logger, Supplier<Void> reloadAction) { ProfileCache profileCache, GuiManager guiManager,
Logger logger, Supplier<Void> reloadAction) {
this.nickService = nickService; this.nickService = nickService;
this.rankService = rankService; this.rankService = rankService;
this.skinService = skinService; this.skinService = skinService;
this.placeholderService = placeholderService; this.placeholderService = placeholderService;
this.nametagService = nametagService; this.nametagService = nametagService;
this.nameOverrideService = nameOverrideService;
this.profileCache = profileCache; this.profileCache = profileCache;
this.guiManager = guiManager; this.guiManager = guiManager;
this.logger = logger; this.logger = logger;
@@ -226,7 +230,8 @@ public final class NickCommand implements CommandExecutor, TabCompleter {
sendMessage(player, "<red>Usage: /nick info <player>"); sendMessage(player, "<red>Usage: /nick info <player>");
return; 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) { if (target == null) {
sendMessage(player, "<red>Player not found."); sendMessage(player, "<red>Player not found.");
return; return;
@@ -253,7 +258,8 @@ public final class NickCommand implements CommandExecutor, TabCompleter {
sendMessage(player, "<red>Usage: /nick force <player> <nickname>"); sendMessage(player, "<red>Usage: /nick force <player> <nickname>");
return; 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) { if (target == null) {
sendMessage(player, "<red>Player not found."); sendMessage(player, "<red>Player not found.");
return; return;
@@ -280,7 +286,8 @@ public final class NickCommand implements CommandExecutor, TabCompleter {
sendMessage(player, "<red>Usage: /nick clear <player>"); sendMessage(player, "<red>Usage: /nick clear <player>");
return; 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) { if (target == null) {
sendMessage(player, "<red>Player not found."); sendMessage(player, "<red>Player not found.");
return; return;
@@ -301,13 +308,8 @@ public final class NickCommand implements CommandExecutor, TabCompleter {
Bukkit.getScheduler().runTask( Bukkit.getScheduler().runTask(
Bukkit.getPluginManager().getPlugin("NickCore"), Bukkit.getPluginManager().getPlugin("NickCore"),
() -> { () -> {
NickProfile profile = profileCache.get(player.getUniqueId()).orElse(null); // Apply full name override (display, player list, custom name, lookup maps)
if (profile != null && profile.effectiveDisplayName() != null) { nameOverrideService.applyFullOverride(player);
player.displayName(mm.deserialize(
InputSanitizer.escapeMiniMessage(profile.effectiveDisplayName())));
} else {
player.displayName(Component.text(player.getName()));
}
nametagService.updateNametag(player); nametagService.updateNametag(player);
}); });
} }

View File

@@ -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.
*
* <p>This ensures that commands like /tpa, /msg, /tp, /tell, /w, /pay, /trade,
* /party, /friend, etc. work correctly when players use nicked names as arguments.</p>
*
* <p>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.</p>
*/
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());
}
}
}

View File

@@ -26,19 +26,21 @@ public final class PlayerConnectionListener implements Listener {
private final SkinService skinService; private final SkinService skinService;
private final PlaceholderService placeholderService; private final PlaceholderService placeholderService;
private final NametagService nametagService; private final NametagService nametagService;
private final NameOverrideService nameOverrideService;
private final SchedulerAdapter scheduler; private final SchedulerAdapter scheduler;
private final Logger logger; private final Logger logger;
private final MiniMessage mm = MiniMessage.miniMessage(); private final MiniMessage mm = MiniMessage.miniMessage();
public PlayerConnectionListener(Plugin plugin, NickService nickService, public PlayerConnectionListener(Plugin plugin, NickService nickService,
SkinService skinService, PlaceholderService placeholderService, SkinService skinService, PlaceholderService placeholderService,
NametagService nametagService, SchedulerAdapter scheduler, NametagService nametagService, NameOverrideService nameOverrideService,
Logger logger) { SchedulerAdapter scheduler, Logger logger) {
this.plugin = plugin; this.plugin = plugin;
this.nickService = nickService; this.nickService = nickService;
this.skinService = skinService; this.skinService = skinService;
this.placeholderService = placeholderService; this.placeholderService = placeholderService;
this.nametagService = nametagService; this.nametagService = nametagService;
this.nameOverrideService = nameOverrideService;
this.scheduler = scheduler; this.scheduler = scheduler;
this.logger = logger; this.logger = logger;
} }
@@ -56,11 +58,8 @@ public final class PlayerConnectionListener implements Listener {
scheduler.runAtEntity(plugin, player, () -> { scheduler.runAtEntity(plugin, player, () -> {
if (!player.isOnline()) return; if (!player.isOnline()) return;
// Apply display name // Apply full name override (display name, player list, custom name, lookup maps)
if (profile.effectiveDisplayName() != null) { nameOverrideService.applyFullOverride(player);
player.displayName(mm.deserialize(
InputSanitizer.escapeMiniMessage(profile.effectiveDisplayName())));
}
// Apply nametag // Apply nametag
nametagService.updateNametag(player); nametagService.updateNametag(player);
@@ -89,6 +88,7 @@ public final class PlayerConnectionListener implements Listener {
}); });
// Cleanup // Cleanup
nameOverrideService.clearMapping(player);
nickService.unloadProfile(player.getUniqueId()); nickService.unloadProfile(player.getUniqueId());
placeholderService.remove(player.getUniqueId()); placeholderService.remove(player.getUniqueId());
nametagService.removeNametag(player); nametagService.removeNametag(player);

View File

@@ -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.
*
* <p>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.</p>
*/
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<String> original = event.getCompletions();
List<String> 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);
}
}
}

View File

@@ -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.
*
* <p>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.</p>
*
* <p>Thread-safe: all maps are ConcurrentHashMap.</p>
*/
public final class NameOverrideService {
private final ProfileCache profileCache;
private final MiniMessage miniMessage = MiniMessage.miniMessage();
/** nick (lowercase) -> real player UUID */
private final ConcurrentHashMap<String, UUID> nickToUuid = new ConcurrentHashMap<>(512);
/** real name (lowercase) -> nick name */
private final ConcurrentHashMap<String, String> 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<Player> 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<String, UUID> getNickMappings() {
return Map.copyOf(nickToUuid);
}
/**
* Clears all mappings. Called on plugin shutdown.
*/
public void clear() {
nickToUuid.clear();
realToNick.clear();
}
}