feat: full name injection - nicks replace real names in tab complete, /tpa, /msg, and all commands
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user