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.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();

View File

@@ -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<Void> reloadAction) {
NametagService nametagService, NameOverrideService nameOverrideService,
ProfileCache profileCache, GuiManager guiManager,
Logger logger, Supplier<Void> 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, "<red>Usage: /nick info <player>");
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, "<red>Player not found.");
return;
@@ -253,7 +258,8 @@ public final class NickCommand implements CommandExecutor, TabCompleter {
sendMessage(player, "<red>Usage: /nick force <player> <nickname>");
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, "<red>Player not found.");
return;
@@ -280,7 +286,8 @@ public final class NickCommand implements CommandExecutor, TabCompleter {
sendMessage(player, "<red>Usage: /nick clear <player>");
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, "<red>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);
});
}

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

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