From c0ffffa09a8cea5681a53b753655751dc43b5adf Mon Sep 17 00:00:00 2001 From: Tau Date: Fri, 28 Aug 2026 16:09:41 -0400 Subject: [PATCH 1/8] Fix command targeting for nicknamed players Written by GPT so may not be perfect. Allow player-target commands to resolve unique Essential Commands nicknames while preserving real usernames and vanilla selectors. --- .../EssentialCommandRegistry.java | 13 +- .../commands/CommandUtil.java | 9 +- .../commands/HomeTeleportOtherCommand.java | 3 +- .../commands/NicknameTargetResolver.java | 135 ++++++++++++++++++ .../commands/TeleportAskCommand.java | 3 +- .../commands/TeleportAskHereCommand.java | 3 +- .../commands/TeleportResponseCommand.java | 3 +- .../commands/WarpTpCommand.java | 3 +- .../mixin/EntitySelectorNicknameMixin.java | 90 ++++++++++++ .../resources/essential_commands.mixins.json | 1 + 10 files changed, 240 insertions(+), 23 deletions(-) create mode 100644 src/main/java/com/fibermc/essentialcommands/commands/NicknameTargetResolver.java create mode 100644 src/main/java/com/fibermc/essentialcommands/mixin/EntitySelectorNicknameMixin.java diff --git a/src/main/java/com/fibermc/essentialcommands/EssentialCommandRegistry.java b/src/main/java/com/fibermc/essentialcommands/EssentialCommandRegistry.java index 9ba2d93d..7b7cdae7 100644 --- a/src/main/java/com/fibermc/essentialcommands/EssentialCommandRegistry.java +++ b/src/main/java/com/fibermc/essentialcommands/EssentialCommandRegistry.java @@ -32,7 +32,6 @@ import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; import net.minecraft.commands.arguments.ComponentArgument; -import net.minecraft.commands.arguments.EntityArgument; import net.minecraft.network.chat.Component; import net.minecraft.server.permissions.Permission; import net.minecraft.server.permissions.PermissionLevel; @@ -84,7 +83,7 @@ public static void register( if (CONFIG.ENABLE_TPA) { registerNode.accept(Commands.literal("tpa") .requires(ECPerms.require(ECPerms.Registry.tpa, 0)) - .then(CommandUtil.targetPlayerArgument() + .then(NicknameTargetResolver.targetPlayerArgument() .executes(new TeleportAskCommand())) .build()); @@ -96,7 +95,7 @@ public static void register( registerNode.accept(Commands.literal("tpaccept") .requires(ECPerms.require(ECPerms.Registry.tpaccept, 0)) .executes(new TeleportAcceptCommand()::runDefault) - .then(CommandUtil.targetPlayerArgument() + .then(NicknameTargetResolver.targetPlayerArgument() .suggests(TeleportResponseSuggestion.STRING_SUGGESTIONS_PROVIDER) .executes(new TeleportAcceptCommand())) .build()); @@ -104,14 +103,14 @@ public static void register( registerNode.accept(Commands.literal("tpdeny") .requires(ECPerms.require(ECPerms.Registry.tpdeny, 0)) .executes(new TeleportDenyCommand()::runDefault) - .then(CommandUtil.targetPlayerArgument() + .then(NicknameTargetResolver.targetPlayerArgument() .suggests(TeleportResponseSuggestion.STRING_SUGGESTIONS_PROVIDER) .executes(new TeleportDenyCommand())) .build()); registerNode.accept(Commands.literal("tpahere") .requires(ECPerms.require(ECPerms.Registry.tpahere, 0)) - .then(CommandUtil.targetPlayerArgument() + .then(NicknameTargetResolver.targetPlayerArgument() .executes(new TeleportAskHereCommand())) .build()); } @@ -148,7 +147,7 @@ public static void register( homeTpOtherBuilder .requires(ECPerms.require(ECPerms.Registry.home_tp_others, 2)) - .then(argument("target_player", EntityArgument.player()) + .then(CommandUtil.targetPlayerArgument() .then(argument("home_name", StringArgumentType.word()) .suggests(HomeTeleportOtherCommand.Suggestion.LIST_SUGGESTION_PROVIDER) .executes(new HomeTeleportOtherCommand()))); @@ -241,7 +240,7 @@ public static void register( warpTpOtherBuilder .requires(ECPerms.require(ECPerms.Registry.home_tp_others, 2)) - .then(argument("target_player", EntityArgument.player()) + .then(CommandUtil.targetPlayerArgument() .then(argument("warp_name", StringArgumentType.word()) .suggests(WarpSuggestion.STRING_SUGGESTIONS_PROVIDER) .executes(new WarpTpCommand()::runOther))); diff --git a/src/main/java/com/fibermc/essentialcommands/commands/CommandUtil.java b/src/main/java/com/fibermc/essentialcommands/commands/CommandUtil.java index 99f67f75..ac90d832 100644 --- a/src/main/java/com/fibermc/essentialcommands/commands/CommandUtil.java +++ b/src/main/java/com/fibermc/essentialcommands/commands/CommandUtil.java @@ -9,9 +9,6 @@ import com.mojang.brigadier.tree.CommandNode; import net.minecraft.commands.CommandSourceStack; -import net.minecraft.commands.Commands; -import net.minecraft.commands.arguments.EntityArgument; -import net.minecraft.commands.arguments.selector.EntitySelector; import net.minecraft.server.level.ServerPlayer; import dev.jpcode.eccore.util.TextUtil; @@ -20,8 +17,8 @@ public final class CommandUtil { private CommandUtil() {} - public static RequiredArgumentBuilder targetPlayerArgument() { - return Commands.argument("target_player", EntityArgument.player()); + public static RequiredArgumentBuilder targetPlayerArgument() { + return NicknameTargetResolver.targetPlayerArgumentNonGreedy(); } public static String getCommandString(CommandSourceStack source, CommandNode commandNode) { @@ -39,7 +36,7 @@ public static CommandSyntaxException createSimpleException(Message msg) { public static ServerPlayer getCommandTargetPlayer(CommandContext context) throws CommandSyntaxException { try { - return EntityArgument.getPlayer(context, "target_player"); + return NicknameTargetResolver.getPlayer(context, "target_player"); } catch (IllegalArgumentException e) { return context.getSource().getPlayer(); } diff --git a/src/main/java/com/fibermc/essentialcommands/commands/HomeTeleportOtherCommand.java b/src/main/java/com/fibermc/essentialcommands/commands/HomeTeleportOtherCommand.java index 9c288d5b..fe0f316b 100644 --- a/src/main/java/com/fibermc/essentialcommands/commands/HomeTeleportOtherCommand.java +++ b/src/main/java/com/fibermc/essentialcommands/commands/HomeTeleportOtherCommand.java @@ -21,7 +21,6 @@ import com.mojang.brigadier.suggestion.SuggestionProvider; import net.minecraft.commands.CommandSourceStack; -import net.minecraft.commands.arguments.EntityArgument; import net.minecraft.network.chat.Component; import static com.fibermc.essentialcommands.EssentialCommands.CONFIG; @@ -36,7 +35,7 @@ public int run(CommandContext context) throws CommandSyntaxE } private static PlayerData getTargetPlayerData(CommandContext context) throws CommandSyntaxException { - return ((ServerPlayerEntityAccess) EntityArgument.getPlayer(context, "target_player")).ec$getPlayerData(); + return ((ServerPlayerEntityAccess) NicknameTargetResolver.getPlayer(context, "target_player")).ec$getPlayerData(); } public int runDefault(CommandContext context) throws CommandSyntaxException { diff --git a/src/main/java/com/fibermc/essentialcommands/commands/NicknameTargetResolver.java b/src/main/java/com/fibermc/essentialcommands/commands/NicknameTargetResolver.java new file mode 100644 index 00000000..3c90e8b0 --- /dev/null +++ b/src/main/java/com/fibermc/essentialcommands/commands/NicknameTargetResolver.java @@ -0,0 +1,135 @@ +package com.fibermc.essentialcommands.commands; + +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +import com.fibermc.essentialcommands.playerdata.PlayerData; +import com.fibermc.essentialcommands.playerdata.PlayerDataManager; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; + +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.commands.arguments.selector.EntitySelector; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; + +/** + * Resolves online players by real username first, then by a unique + * Essential Commands nickname. + */ +public final class NicknameTargetResolver { + private NicknameTargetResolver() {} + + /** + * Greedy player argument for commands where the target is the final + * argument. This allows unquoted nicknames containing spaces. + */ + public static RequiredArgumentBuilder targetPlayerArgument() { + return Commands.argument("target_player", StringArgumentType.greedyString()) + .suggests((context, builder) -> suggestPlayers(context, builder, false)); + } + + /** + * Non-greedy player argument for commands that have arguments after the + * target. Nicknames containing spaces can be quoted. + */ + public static RequiredArgumentBuilder targetPlayerArgumentNonGreedy() { + return Commands.argument("target_player", StringArgumentType.string()) + .suggests((context, builder) -> suggestPlayers(context, builder, true)); + } + + private static CompletableFuture suggestPlayers( + CommandContext context, + SuggestionsBuilder builder, + boolean quoteSuggestions + ) throws CommandSyntaxException { + if (builder.getRemaining().startsWith("@")) { + return EntityArgument.player().listSuggestions(context, builder); + } + + var names = new LinkedHashSet(); + for (ServerPlayer player : context.getSource().getServer().getPlayerList().getPlayers()) { + names.add(player.getGameProfile().name()); + PlayerData.access(player) + .getNickname() + .map(Component::getString) + .filter(nickname -> !nickname.isBlank()) + .ifPresent(names::add); + } + + String remaining = builder.getRemaining(); + String prefix = remaining.startsWith("\"") ? remaining.substring(1) : remaining; + prefix = prefix.toLowerCase(Locale.ROOT); + + for (String name : names) { + if (name.toLowerCase(Locale.ROOT).startsWith(prefix)) { + builder.suggest(quoteSuggestions ? quoteIfNeeded(name) : name); + } + } + return builder.buildFuture(); + } + + private static String quoteIfNeeded(String value) { + if (value.indexOf(' ') < 0 && value.indexOf('"') < 0 && value.indexOf('\\') < 0) { + return value; + } + return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\""; + } + + public static ServerPlayer getPlayer( + CommandContext context, + String argumentName + ) throws CommandSyntaxException { + String target = StringArgumentType.getString(context, argumentName).trim(); + + // Preserve normal vanilla selector behavior. + if (target.startsWith("@")) { + EntitySelector selector = EntityArgument.player().parse( + new StringReader(target), + context.getSource() + ); + return selector.findSinglePlayer(context.getSource()); + } + + // Real Minecraft usernames always win over nickname collisions. + ServerPlayer usernameMatch = context + .getSource() + .getServer() + .getPlayerList() + .getPlayerByName(target); + if (usernameMatch != null) { + return usernameMatch; + } + + var nicknameMatches = PlayerDataManager + .getInstance() + .getPlayerDataMatchingNickname(target) + .stream() + .map(PlayerData::getPlayer) + .filter(Objects::nonNull) + .toList(); + + if (nicknameMatches.size() == 1) { + return nicknameMatches.get(0); + } + if (nicknameMatches.size() > 1) { + throw CommandUtil.createSimpleException(Component.literal( + "Nickname \"" + target + "\" matches more than one online player. Use the real username instead." + )); + } + + throw CommandUtil.createSimpleException(Component.literal( + "No online player found with username or nickname \"" + target + "\"." + )); + } +} diff --git a/src/main/java/com/fibermc/essentialcommands/commands/TeleportAskCommand.java b/src/main/java/com/fibermc/essentialcommands/commands/TeleportAskCommand.java index 58f1215e..4c4f63e3 100644 --- a/src/main/java/com/fibermc/essentialcommands/commands/TeleportAskCommand.java +++ b/src/main/java/com/fibermc/essentialcommands/commands/TeleportAskCommand.java @@ -14,7 +14,6 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException; import net.minecraft.commands.CommandSourceStack; -import net.minecraft.commands.arguments.EntityArgument; import net.minecraft.server.level.ServerPlayer; public class TeleportAskCommand implements Command { @@ -25,7 +24,7 @@ public TeleportAskCommand() {} public int run(CommandContext context) throws CommandSyntaxException { TeleportManager tpMgr = ManagerLocator.getInstance().getTpManager(); ServerPlayer senderPlayer = context.getSource().getPlayerOrException(); - ServerPlayer targetPlayer = EntityArgument.getPlayer(context, "target_player"); + ServerPlayer targetPlayer = NicknameTargetResolver.getPlayer(context, "target_player"); var senderPlayerData = PlayerData.access(senderPlayer); var targetPlayerData = PlayerData.access(targetPlayer); diff --git a/src/main/java/com/fibermc/essentialcommands/commands/TeleportAskHereCommand.java b/src/main/java/com/fibermc/essentialcommands/commands/TeleportAskHereCommand.java index d6d51143..4d40882d 100644 --- a/src/main/java/com/fibermc/essentialcommands/commands/TeleportAskHereCommand.java +++ b/src/main/java/com/fibermc/essentialcommands/commands/TeleportAskHereCommand.java @@ -12,7 +12,6 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException; import net.minecraft.commands.CommandSourceStack; -import net.minecraft.commands.arguments.EntityArgument; import net.minecraft.server.level.ServerPlayer; public class TeleportAskHereCommand implements Command { @@ -23,7 +22,7 @@ public TeleportAskHereCommand() {} public int run(CommandContext context) throws CommandSyntaxException { TeleportManager tpMgr = ManagerLocator.getInstance().getTpManager(); ServerPlayer senderPlayer = context.getSource().getPlayerOrException(); - ServerPlayer targetPlayer = EntityArgument.getPlayer(context, "target_player"); + ServerPlayer targetPlayer = NicknameTargetResolver.getPlayer(context, "target_player"); var senderPlayerData = PlayerData.access(senderPlayer); var targetPlayerData = PlayerData.access(targetPlayer); diff --git a/src/main/java/com/fibermc/essentialcommands/commands/TeleportResponseCommand.java b/src/main/java/com/fibermc/essentialcommands/commands/TeleportResponseCommand.java index a6cd0659..64a44053 100644 --- a/src/main/java/com/fibermc/essentialcommands/commands/TeleportResponseCommand.java +++ b/src/main/java/com/fibermc/essentialcommands/commands/TeleportResponseCommand.java @@ -13,7 +13,6 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException; import net.minecraft.commands.CommandSourceStack; -import net.minecraft.commands.arguments.EntityArgument; import net.minecraft.server.level.ServerPlayer; public abstract class TeleportResponseCommand implements Command { @@ -22,7 +21,7 @@ public int run(CommandContext context) throws CommandSyntaxE return exec( context, context.getSource().getPlayer(), - EntityArgument.getPlayer(context, "target_player") + NicknameTargetResolver.getPlayer(context, "target_player") ); } diff --git a/src/main/java/com/fibermc/essentialcommands/commands/WarpTpCommand.java b/src/main/java/com/fibermc/essentialcommands/commands/WarpTpCommand.java index 944ce037..5a66a53e 100644 --- a/src/main/java/com/fibermc/essentialcommands/commands/WarpTpCommand.java +++ b/src/main/java/com/fibermc/essentialcommands/commands/WarpTpCommand.java @@ -12,7 +12,6 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException; import net.minecraft.commands.CommandSourceStack; -import net.minecraft.commands.arguments.EntityArgument; import net.minecraft.server.level.ServerPlayer; public class WarpTpCommand implements Command { @@ -61,7 +60,7 @@ private void exec( } public int runOther(CommandContext context) throws CommandSyntaxException { - exec(context, EntityArgument.getPlayer(context, "target_player")); + exec(context, NicknameTargetResolver.getPlayer(context, "target_player")); return 0; } } diff --git a/src/main/java/com/fibermc/essentialcommands/mixin/EntitySelectorNicknameMixin.java b/src/main/java/com/fibermc/essentialcommands/mixin/EntitySelectorNicknameMixin.java new file mode 100644 index 00000000..606c4969 --- /dev/null +++ b/src/main/java/com/fibermc/essentialcommands/mixin/EntitySelectorNicknameMixin.java @@ -0,0 +1,90 @@ +package com.fibermc.essentialcommands.mixin; + +import java.util.Collections; +import java.util.List; + +import com.fibermc.essentialcommands.playerdata.PlayerData; +import com.fibermc.essentialcommands.playerdata.PlayerDataManager; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.selector.EntitySelector; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; + +/** + * Adds Essential Commands nickname fallback to vanilla literal player + * selectors. Vanilla usernames retain priority, and @ selectors are + * unaffected because they do not populate EntitySelector.playerName. + */ +@Mixin(EntitySelector.class) +public abstract class EntitySelectorNicknameMixin { + @Shadow + @Final + private String playerName; + + @Inject(method = "findPlayers", at = @At("HEAD"), cancellable = true) + private void ec$resolveNicknameForPlayers( + CommandSourceStack source, + CallbackInfoReturnable> cir + ) { + ServerPlayer player = ec$resolveLiteralPlayer(source); + if (player != null) { + cir.setReturnValue(Collections.singletonList(player)); + } + } + + @Inject(method = "findEntities", at = @At("HEAD"), cancellable = true) + private void ec$resolveNicknameForEntities( + CommandSourceStack source, + CallbackInfoReturnable> cir + ) { + ServerPlayer player = ec$resolveLiteralPlayer(source); + if (player != null) { + cir.setReturnValue(Collections.singletonList(player)); + } + } + + private ServerPlayer ec$resolveLiteralPlayer(CommandSourceStack source) { + if (this.playerName == null || this.playerName.isBlank()) { + return null; + } + + // Preserve vanilla username precedence. + ServerPlayer usernameMatch = source + .getServer() + .getPlayerList() + .getPlayerByName(this.playerName); + if (usernameMatch != null) { + return usernameMatch; + } + + if (!PlayerDataManager.exists()) { + return null; + } + + List matches = PlayerDataManager + .getInstance() + .getPlayerDataMatchingNickname(this.playerName); + + ServerPlayer nicknameMatch = null; + for (PlayerData playerData : matches) { + ServerPlayer player = playerData.getPlayer(); + if (player == null) { + continue; + } + if (nicknameMatch != null && nicknameMatch != player) { + // Ambiguous nickname: let vanilla fail rather than selecting + // an arbitrary player. + return null; + } + nicknameMatch = player; + } + return nicknameMatch; + } +} diff --git a/src/main/resources/essential_commands.mixins.json b/src/main/resources/essential_commands.mixins.json index 21d014cc..d7dbf312 100644 --- a/src/main/resources/essential_commands.mixins.json +++ b/src/main/resources/essential_commands.mixins.json @@ -13,6 +13,7 @@ "ServerGamePacketListenerImplMixin", "ServerScoreboardMixin", "SleepStatusMixin", + "EntitySelectorNicknameMixin", "TeleportCommandMixin", "PlayerDataStorageMixin" ], From 7c0e4db278ee015f97918672485bf9fdf6c1bd23 Mon Sep 17 00:00:00 2001 From: Tau <_Tau@outlook.com> Date: Sat, 29 Aug 2026 14:10:43 -0400 Subject: [PATCH 2/8] Refactor nickname targeting based on feedback Add configurable nickname argument scope and use vanilla-first nickname fallback with shared resolution logic. --- .../access/EntitySelectorNicknameAccess.java | 5 + .../commands/CommandUtil.java | 4 +- .../commands/NicknameTargetResolver.java | 188 ++++++++++-------- .../config/EssentialCommandsConfig.java | 2 + .../EssentialCommandsConfigSnapshot.java | 3 + .../mixin/EntitySelectorNicknameMixin.java | 83 +++----- .../types/NicknameCommandArgMode.java | 7 + 7 files changed, 155 insertions(+), 137 deletions(-) create mode 100644 src/main/java/com/fibermc/essentialcommands/access/EntitySelectorNicknameAccess.java create mode 100644 src/main/java/com/fibermc/essentialcommands/types/NicknameCommandArgMode.java diff --git a/src/main/java/com/fibermc/essentialcommands/access/EntitySelectorNicknameAccess.java b/src/main/java/com/fibermc/essentialcommands/access/EntitySelectorNicknameAccess.java new file mode 100644 index 00000000..3dc5333c --- /dev/null +++ b/src/main/java/com/fibermc/essentialcommands/access/EntitySelectorNicknameAccess.java @@ -0,0 +1,5 @@ +package com.fibermc.essentialcommands.access; + +public interface EntitySelectorNicknameAccess { + String ec$getPlayerName(); +} diff --git a/src/main/java/com/fibermc/essentialcommands/commands/CommandUtil.java b/src/main/java/com/fibermc/essentialcommands/commands/CommandUtil.java index ac90d832..a931acad 100644 --- a/src/main/java/com/fibermc/essentialcommands/commands/CommandUtil.java +++ b/src/main/java/com/fibermc/essentialcommands/commands/CommandUtil.java @@ -7,8 +7,8 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; import com.mojang.brigadier.tree.CommandNode; - import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.selector.EntitySelector; import net.minecraft.server.level.ServerPlayer; import dev.jpcode.eccore.util.TextUtil; @@ -17,7 +17,7 @@ public final class CommandUtil { private CommandUtil() {} - public static RequiredArgumentBuilder targetPlayerArgument() { + public static RequiredArgumentBuilder targetPlayerArgument() { return NicknameTargetResolver.targetPlayerArgumentNonGreedy(); } diff --git a/src/main/java/com/fibermc/essentialcommands/commands/NicknameTargetResolver.java b/src/main/java/com/fibermc/essentialcommands/commands/NicknameTargetResolver.java index 3c90e8b0..72ad35e5 100644 --- a/src/main/java/com/fibermc/essentialcommands/commands/NicknameTargetResolver.java +++ b/src/main/java/com/fibermc/essentialcommands/commands/NicknameTargetResolver.java @@ -2,20 +2,17 @@ import java.util.LinkedHashSet; import java.util.Locale; -import java.util.Objects; import java.util.concurrent.CompletableFuture; +import com.fibermc.essentialcommands.access.EntitySelectorNicknameAccess; import com.fibermc.essentialcommands.playerdata.PlayerData; import com.fibermc.essentialcommands.playerdata.PlayerDataManager; - -import com.mojang.brigadier.StringReader; -import com.mojang.brigadier.arguments.StringArgumentType; +import com.fibermc.essentialcommands.types.NicknameCommandArgMode; import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.SuggestionsBuilder; - import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; import net.minecraft.commands.arguments.EntityArgument; @@ -23,113 +20,136 @@ import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerPlayer; +import static com.fibermc.essentialcommands.EssentialCommands.CONFIG; + /** - * Resolves online players by real username first, then by a unique - * Essential Commands nickname. + * Adds optional nickname fallback on top of Minecraft's normal player + * argument handling. Vanilla resolution is always attempted first. */ public final class NicknameTargetResolver { private NicknameTargetResolver() {} - /** - * Greedy player argument for commands where the target is the final - * argument. This allows unquoted nicknames containing spaces. - */ - public static RequiredArgumentBuilder targetPlayerArgument() { - return Commands.argument("target_player", StringArgumentType.greedyString()) - .suggests((context, builder) -> suggestPlayers(context, builder, false)); + public static RequiredArgumentBuilder targetPlayerArgument() { + return Commands.argument("target_player", EntityArgument.player()) + .suggests(NicknameTargetResolver::suggestPlayers); } - /** - * Non-greedy player argument for commands that have arguments after the - * target. Nicknames containing spaces can be quoted. - */ - public static RequiredArgumentBuilder targetPlayerArgumentNonGreedy() { - return Commands.argument("target_player", StringArgumentType.string()) - .suggests((context, builder) -> suggestPlayers(context, builder, true)); + public static RequiredArgumentBuilder targetPlayerArgumentNonGreedy() { + return targetPlayerArgument(); } private static CompletableFuture suggestPlayers( CommandContext context, - SuggestionsBuilder builder, - boolean quoteSuggestions + SuggestionsBuilder builder ) throws CommandSyntaxException { - if (builder.getRemaining().startsWith("@")) { - return EntityArgument.player().listSuggestions(context, builder); - } - - var names = new LinkedHashSet(); - for (ServerPlayer player : context.getSource().getServer().getPlayerList().getPlayers()) { - names.add(player.getGameProfile().name()); - PlayerData.access(player) - .getNickname() - .map(Component::getString) - .filter(nickname -> !nickname.isBlank()) - .ifPresent(names::add); - } - - String remaining = builder.getRemaining(); - String prefix = remaining.startsWith("\"") ? remaining.substring(1) : remaining; - prefix = prefix.toLowerCase(Locale.ROOT); + if ( + CONFIG.NICKNAMES_AS_COMMAND_ARG != NicknameCommandArgMode.Never + && PlayerDataManager.exists() + ) { + String remaining = builder.getRemaining().toLowerCase(Locale.ROOT); + var nicknameSuggestions = new LinkedHashSet(); + + for (PlayerData playerData : PlayerDataManager.getInstance().getAllPlayerData()) { + playerData.getNickname() + .map(Component::getString) + .map(NicknameTargetResolver::normalizeNickname) + .filter(nickname -> !nickname.isBlank()) + .ifPresent(nicknameSuggestions::add); + } - for (String name : names) { - if (name.toLowerCase(Locale.ROOT).startsWith(prefix)) { - builder.suggest(quoteSuggestions ? quoteIfNeeded(name) : name); + for (String nickname : nicknameSuggestions) { + if (nickname.toLowerCase(Locale.ROOT).startsWith(remaining)) { + builder.suggest(nickname); + } } } - return builder.buildFuture(); - } - private static String quoteIfNeeded(String value) { - if (value.indexOf(' ') < 0 && value.indexOf('"') < 0 && value.indexOf('\\') < 0) { - return value; - } - return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\""; + return EntityArgument.player().listSuggestions(context, builder); } + /** + * Resolves an Essential Commands player argument. + * + * Everywhere: + * EntityArgument resolves nicknames through EntitySelectorNicknameMixin. + * EssentialCommandsOnly: + * Vanilla is attempted first, then the shared literal nickname fallback. + * Never: + * Vanilla behavior is used unchanged. + */ public static ServerPlayer getPlayer( CommandContext context, String argumentName ) throws CommandSyntaxException { - String target = StringArgumentType.getString(context, argumentName).trim(); - - // Preserve normal vanilla selector behavior. - if (target.startsWith("@")) { - EntitySelector selector = EntityArgument.player().parse( - new StringReader(target), - context.getSource() - ); - return selector.findSinglePlayer(context.getSource()); - } + try { + return EntityArgument.getPlayer(context, argumentName); + } catch (CommandSyntaxException vanillaFailure) { + if (CONFIG.NICKNAMES_AS_COMMAND_ARG != NicknameCommandArgMode.EssentialCommandsOnly) { + throw vanillaFailure; + } + + EntitySelector selector = context.getArgument(argumentName, EntitySelector.class); + String playerName = ((EntitySelectorNicknameAccess) (Object) selector).ec$getPlayerName(); + ServerPlayer nicknameMatch = resolveLiteralPlayer(playerName); + if (nicknameMatch != null) { + return nicknameMatch; + } - // Real Minecraft usernames always win over nickname collisions. - ServerPlayer usernameMatch = context - .getSource() - .getServer() - .getPlayerList() - .getPlayerByName(target); - if (usernameMatch != null) { - return usernameMatch; + throw vanillaFailure; } + } - var nicknameMatches = PlayerDataManager - .getInstance() - .getPlayerDataMatchingNickname(target) - .stream() - .map(PlayerData::getPlayer) - .filter(Objects::nonNull) - .toList(); + /** + * Finds one online player whose nickname matches the literal command + * player name after whitespace normalization. + * + * Returns null for no match or an ambiguous normalized nickname. + */ + public static ServerPlayer resolveLiteralPlayer(String playerName) { + if ( + playerName == null + || playerName.isBlank() + || !PlayerDataManager.exists() + ) { + return null; + } - if (nicknameMatches.size() == 1) { - return nicknameMatches.get(0); + String target = normalizeNickname(playerName); + if (target.isBlank()) { + return null; } - if (nicknameMatches.size() > 1) { - throw CommandUtil.createSimpleException(Component.literal( - "Nickname \"" + target + "\" matches more than one online player. Use the real username instead." - )); + + ServerPlayer nicknameMatch = null; + for (PlayerData playerData : PlayerDataManager.getInstance().getAllPlayerData()) { + boolean matches = playerData + .getNickname() + .map(Component::getString) + .map(NicknameTargetResolver::normalizeNickname) + .map(nickname -> nickname.equalsIgnoreCase(target)) + .orElse(false); + + if (!matches) { + continue; + } + + ServerPlayer player = playerData.getPlayer(); + if (player == null) { + continue; + } + + if (nicknameMatch != null && nicknameMatch != player) { + // Normalization can make different nicknames collide, e.g. + // "Foo Bar" and "FooBar". Never choose one arbitrarily. + return null; + } + + nicknameMatch = player; } - throw CommandUtil.createSimpleException(Component.literal( - "No online player found with username or nickname \"" + target + "\"." - )); + return nicknameMatch; + } + + private static String normalizeNickname(String nickname) { + return nickname.replaceAll("\\s+", ""); } } diff --git a/src/main/java/com/fibermc/essentialcommands/config/EssentialCommandsConfig.java b/src/main/java/com/fibermc/essentialcommands/config/EssentialCommandsConfig.java index a1c06f57..9b83b6b2 100644 --- a/src/main/java/com/fibermc/essentialcommands/config/EssentialCommandsConfig.java +++ b/src/main/java/com/fibermc/essentialcommands/config/EssentialCommandsConfig.java @@ -16,6 +16,7 @@ import com.fibermc.essentialcommands.EssentialCommands; import com.fibermc.essentialcommands.ManagerLocator; import com.fibermc.essentialcommands.playerdata.PlayerDataManager; +import com.fibermc.essentialcommands.types.NicknameCommandArgMode; import com.fibermc.essentialcommands.types.RespawnCondition; import com.fibermc.essentialcommands.types.RtpCenter; import org.jetbrains.annotations.NotNull; @@ -85,6 +86,7 @@ public final class EssentialCommandsConfig extends Config OPS_BYPASS_TELEPORT_RULES = new Option<>("ops_bypass_teleport_rules", true, Boolean::parseBoolean); @ConfigOption public final Option NICKNAMES_IN_PLAYER_LIST = new Option<>("nicknames_in_player_list", true, Boolean::parseBoolean); @ConfigOption public final Option NICKNAME_MAX_LENGTH = new Option<>("nickname_max_length", 32, ConfigUtil::parseInt); + @ConfigOption public final Option NICKNAMES_AS_COMMAND_ARG = new Option<>("nicknames_as_command_arg", NicknameCommandArgMode.Never, NicknameCommandArgMode::valueOf); @ConfigOption public final Option NICKNAME_ABOVE_HEAD = new Option<>("nickname_above_head", false, Boolean::parseBoolean); @ConfigOption public final Option RTP_CENTER = new Option<>("rtp_center", RtpCenter.spawn(), RtpCenter::parse, RtpCenter::serialize); @ConfigOption public final Option RTP_RADIUS = new Option<>("rtp_radius", 1000, ConfigUtil::parseInt); diff --git a/src/main/java/com/fibermc/essentialcommands/config/EssentialCommandsConfigSnapshot.java b/src/main/java/com/fibermc/essentialcommands/config/EssentialCommandsConfigSnapshot.java index 49f3df9b..77a4720f 100644 --- a/src/main/java/com/fibermc/essentialcommands/config/EssentialCommandsConfigSnapshot.java +++ b/src/main/java/com/fibermc/essentialcommands/config/EssentialCommandsConfigSnapshot.java @@ -6,6 +6,7 @@ import net.minecraft.network.chat.Style; import net.minecraft.resources.ResourceKey; import net.minecraft.world.level.Level; +import com.fibermc.essentialcommands.types.NicknameCommandArgMode; import com.fibermc.essentialcommands.types.RespawnCondition; import com.fibermc.essentialcommands.types.RtpCenter; import dev.jpcode.eccore.config.expression.Expression; @@ -64,6 +65,7 @@ public final class EssentialCommandsConfigSnapshot { public final boolean OPS_BYPASS_TELEPORT_RULES; public final boolean NICKNAMES_IN_PLAYER_LIST; public final int NICKNAME_MAX_LENGTH; + public final NicknameCommandArgMode NICKNAMES_AS_COMMAND_ARG; public final boolean NICKNAME_ABOVE_HEAD; public final RtpCenter RTP_CENTER; public final int RTP_RADIUS; @@ -143,6 +145,7 @@ private EssentialCommandsConfigSnapshot(EssentialCommandsConfig config) { this.OPS_BYPASS_TELEPORT_RULES = config.OPS_BYPASS_TELEPORT_RULES.getValue(); this.NICKNAMES_IN_PLAYER_LIST = config.NICKNAMES_IN_PLAYER_LIST.getValue(); this.NICKNAME_MAX_LENGTH = config.NICKNAME_MAX_LENGTH.getValue(); + this.NICKNAMES_AS_COMMAND_ARG = config.NICKNAMES_AS_COMMAND_ARG.getValue(); this.NICKNAME_ABOVE_HEAD = config.NICKNAME_ABOVE_HEAD.getValue(); this.RTP_CENTER = config.RTP_CENTER.getValue(); this.RTP_RADIUS = config.RTP_RADIUS.getValue(); diff --git a/src/main/java/com/fibermc/essentialcommands/mixin/EntitySelectorNicknameMixin.java b/src/main/java/com/fibermc/essentialcommands/mixin/EntitySelectorNicknameMixin.java index 606c4969..aa6ab5e7 100644 --- a/src/main/java/com/fibermc/essentialcommands/mixin/EntitySelectorNicknameMixin.java +++ b/src/main/java/com/fibermc/essentialcommands/mixin/EntitySelectorNicknameMixin.java @@ -1,90 +1,71 @@ package com.fibermc.essentialcommands.mixin; -import java.util.Collections; import java.util.List; -import com.fibermc.essentialcommands.playerdata.PlayerData; -import com.fibermc.essentialcommands.playerdata.PlayerDataManager; +import com.fibermc.essentialcommands.access.EntitySelectorNicknameAccess; +import com.fibermc.essentialcommands.commands.NicknameTargetResolver; +import com.fibermc.essentialcommands.types.NicknameCommandArgMode; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.arguments.selector.EntitySelector; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.Entity; +import static com.fibermc.essentialcommands.EssentialCommands.CONFIG; + /** - * Adds Essential Commands nickname fallback to vanilla literal player - * selectors. Vanilla usernames retain priority, and @ selectors are - * unaffected because they do not populate EntitySelector.playerName. + * Adds nickname fallback after vanilla EntitySelector resolution when global + * nickname command arguments are enabled. */ @Mixin(EntitySelector.class) -public abstract class EntitySelectorNicknameMixin { +public abstract class EntitySelectorNicknameMixin implements EntitySelectorNicknameAccess { @Shadow @Final private String playerName; - @Inject(method = "findPlayers", at = @At("HEAD"), cancellable = true) + @Override + public String ec$getPlayerName() { + return this.playerName; + } + + @Inject(method = "findPlayers", at = @At("RETURN"), cancellable = true) private void ec$resolveNicknameForPlayers( CommandSourceStack source, CallbackInfoReturnable> cir ) { - ServerPlayer player = ec$resolveLiteralPlayer(source); + if ( + CONFIG.NICKNAMES_AS_COMMAND_ARG != NicknameCommandArgMode.Everywhere + || !cir.getReturnValue().isEmpty() + ) { + return; + } + + ServerPlayer player = NicknameTargetResolver.resolveLiteralPlayer(this.playerName); if (player != null) { - cir.setReturnValue(Collections.singletonList(player)); + cir.setReturnValue(List.of(player)); } } - @Inject(method = "findEntities", at = @At("HEAD"), cancellable = true) + @Inject(method = "findEntities", at = @At("RETURN"), cancellable = true) private void ec$resolveNicknameForEntities( CommandSourceStack source, CallbackInfoReturnable> cir ) { - ServerPlayer player = ec$resolveLiteralPlayer(source); - if (player != null) { - cir.setReturnValue(Collections.singletonList(player)); - } - } - - private ServerPlayer ec$resolveLiteralPlayer(CommandSourceStack source) { - if (this.playerName == null || this.playerName.isBlank()) { - return null; - } - - // Preserve vanilla username precedence. - ServerPlayer usernameMatch = source - .getServer() - .getPlayerList() - .getPlayerByName(this.playerName); - if (usernameMatch != null) { - return usernameMatch; + if ( + CONFIG.NICKNAMES_AS_COMMAND_ARG != NicknameCommandArgMode.Everywhere + || !cir.getReturnValue().isEmpty() + ) { + return; } - if (!PlayerDataManager.exists()) { - return null; - } - - List matches = PlayerDataManager - .getInstance() - .getPlayerDataMatchingNickname(this.playerName); - - ServerPlayer nicknameMatch = null; - for (PlayerData playerData : matches) { - ServerPlayer player = playerData.getPlayer(); - if (player == null) { - continue; - } - if (nicknameMatch != null && nicknameMatch != player) { - // Ambiguous nickname: let vanilla fail rather than selecting - // an arbitrary player. - return null; - } - nicknameMatch = player; + ServerPlayer player = NicknameTargetResolver.resolveLiteralPlayer(this.playerName); + if (player != null) { + cir.setReturnValue(List.of(player)); } - return nicknameMatch; } } diff --git a/src/main/java/com/fibermc/essentialcommands/types/NicknameCommandArgMode.java b/src/main/java/com/fibermc/essentialcommands/types/NicknameCommandArgMode.java new file mode 100644 index 00000000..8ea59753 --- /dev/null +++ b/src/main/java/com/fibermc/essentialcommands/types/NicknameCommandArgMode.java @@ -0,0 +1,7 @@ +package com.fibermc.essentialcommands.types; + +public enum NicknameCommandArgMode { + Everywhere, + EssentialCommandsOnly, + Never +} From 92b26e5f534c526c198048a5dd7962c90b59a531 Mon Sep 17 00:00:00 2001 From: John Paul R Date: Sun, 30 Aug 2026 13:53:36 -0400 Subject: [PATCH 3/8] use a map for resolving players by nickname --- .../commands/NicknameTargetResolver.java | 73 +++++-------------- .../mixin/EntitySelectorNicknameMixin.java | 4 +- .../playerdata/PlayerData.java | 21 +++++- .../playerdata/PlayerDataManager.java | 57 +++++++++++---- 4 files changed, 82 insertions(+), 73 deletions(-) diff --git a/src/main/java/com/fibermc/essentialcommands/commands/NicknameTargetResolver.java b/src/main/java/com/fibermc/essentialcommands/commands/NicknameTargetResolver.java index 72ad35e5..f437a991 100644 --- a/src/main/java/com/fibermc/essentialcommands/commands/NicknameTargetResolver.java +++ b/src/main/java/com/fibermc/essentialcommands/commands/NicknameTargetResolver.java @@ -1,7 +1,6 @@ package com.fibermc.essentialcommands.commands; -import java.util.LinkedHashSet; -import java.util.Locale; +import java.util.List; import java.util.concurrent.CompletableFuture; import com.fibermc.essentialcommands.access.EntitySelectorNicknameAccess; @@ -46,21 +45,19 @@ private static CompletableFuture suggestPlayers( CONFIG.NICKNAMES_AS_COMMAND_ARG != NicknameCommandArgMode.Never && PlayerDataManager.exists() ) { - String remaining = builder.getRemaining().toLowerCase(Locale.ROOT); - var nicknameSuggestions = new LinkedHashSet(); + String remaining = PlayerData.normalizeNickname(builder.getRemaining()); for (PlayerData playerData : PlayerDataManager.getInstance().getAllPlayerData()) { + String normalized = playerData.getNormalizedNickname(); + if (normalized == null || !normalized.startsWith(remaining)) { + continue; + } + // Suggest whitespace-stripped but case-preserved form playerData.getNickname() .map(Component::getString) - .map(NicknameTargetResolver::normalizeNickname) - .filter(nickname -> !nickname.isBlank()) - .ifPresent(nicknameSuggestions::add); - } - - for (String nickname : nicknameSuggestions) { - if (nickname.toLowerCase(Locale.ROOT).startsWith(remaining)) { - builder.suggest(nickname); - } + .map(nick -> nick.replaceAll("\\s+", "")) + .filter(nick -> !nick.isBlank()) + .ifPresent(builder::suggest); } } @@ -89,8 +86,8 @@ public static ServerPlayer getPlayer( } EntitySelector selector = context.getArgument(argumentName, EntitySelector.class); - String playerName = ((EntitySelectorNicknameAccess) (Object) selector).ec$getPlayerName(); - ServerPlayer nicknameMatch = resolveLiteralPlayer(playerName); + String playerName = ((EntitySelectorNicknameAccess) selector).ec$getPlayerName(); + ServerPlayer nicknameMatch = resolvePlayerByNickname(playerName); if (nicknameMatch != null) { return nicknameMatch; } @@ -101,55 +98,23 @@ public static ServerPlayer getPlayer( /** * Finds one online player whose nickname matches the literal command - * player name after whitespace normalization. + * player name after normalization (whitespace removal + case folding). * - * Returns null for no match or an ambiguous normalized nickname. + * Returns null for no match or an ambiguous match. */ - public static ServerPlayer resolveLiteralPlayer(String playerName) { + public static ServerPlayer resolvePlayerByNickname(String playerName) { if ( playerName == null || playerName.isBlank() - || !PlayerDataManager.exists() ) { return null; } - String target = normalizeNickname(playerName); - if (target.isBlank()) { + List matches = PlayerDataManager.getInstance().getByNickname(playerName); + // Ambiguous (>1) or no match -- never choose one arbitrarily. + if (matches.size() != 1) { return null; } - - ServerPlayer nicknameMatch = null; - for (PlayerData playerData : PlayerDataManager.getInstance().getAllPlayerData()) { - boolean matches = playerData - .getNickname() - .map(Component::getString) - .map(NicknameTargetResolver::normalizeNickname) - .map(nickname -> nickname.equalsIgnoreCase(target)) - .orElse(false); - - if (!matches) { - continue; - } - - ServerPlayer player = playerData.getPlayer(); - if (player == null) { - continue; - } - - if (nicknameMatch != null && nicknameMatch != player) { - // Normalization can make different nicknames collide, e.g. - // "Foo Bar" and "FooBar". Never choose one arbitrarily. - return null; - } - - nicknameMatch = player; - } - - return nicknameMatch; - } - - private static String normalizeNickname(String nickname) { - return nickname.replaceAll("\\s+", ""); + return matches.getFirst().getPlayer(); } } diff --git a/src/main/java/com/fibermc/essentialcommands/mixin/EntitySelectorNicknameMixin.java b/src/main/java/com/fibermc/essentialcommands/mixin/EntitySelectorNicknameMixin.java index aa6ab5e7..c379962c 100644 --- a/src/main/java/com/fibermc/essentialcommands/mixin/EntitySelectorNicknameMixin.java +++ b/src/main/java/com/fibermc/essentialcommands/mixin/EntitySelectorNicknameMixin.java @@ -45,7 +45,7 @@ public abstract class EntitySelectorNicknameMixin implements EntitySelectorNickn return; } - ServerPlayer player = NicknameTargetResolver.resolveLiteralPlayer(this.playerName); + ServerPlayer player = NicknameTargetResolver.resolvePlayerByNickname(this.playerName); if (player != null) { cir.setReturnValue(List.of(player)); } @@ -63,7 +63,7 @@ public abstract class EntitySelectorNicknameMixin implements EntitySelectorNickn return; } - ServerPlayer player = NicknameTargetResolver.resolveLiteralPlayer(this.playerName); + ServerPlayer player = NicknameTargetResolver.resolvePlayerByNickname(this.playerName); if (player != null) { cir.setReturnValue(List.of(player)); } diff --git a/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerData.java b/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerData.java index 2367221f..cd184d6a 100644 --- a/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerData.java +++ b/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerData.java @@ -26,6 +26,7 @@ import io.github.ladysnake.pal.VanillaAbilities; import me.drex.vanish.api.VanishAPI; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; @@ -77,6 +78,7 @@ public class PlayerData extends SavedData implements IServerPlayerEntityData, IF // Nickname private Component nickname; + private String normalizedNickname; private MutableComponent fullNickname; // RTP Cooldown @@ -133,6 +135,7 @@ public static PlayerData createWithData( pd.homes = homes.orElseGet(NamedLocationStorage::new); pd.previousLocation = previousLocation.orElse(null); pd.nickname = nickname.orElse(null); + pd.normalizedNickname = nickname.map(n -> normalizeNickname(n.getString())).orElse(null); pd.timeUsedRtp = TimeUtil.epochTimeMsToTicks(timeUsedRtpEpochMs); pd.tpCooldown = tpCooldown; return pd; @@ -331,7 +334,7 @@ public void setAfk(boolean afk) { } } - PlayerDataManager.getInstance().markNicknameDirty(this); + PlayerDataManager.getInstance().markNicknameDirty(this, this.normalizedNickname); } public boolean isAfk() { @@ -491,6 +494,17 @@ public Optional getNickname() { return Optional.ofNullable(nickname != null ? nickname.copy() : null); } + // Whitespace is stripped because command arguments cannot contain spaces, + // so "Foo Bar" must be reachable as "FooBar". + public static String normalizeNickname(String nickname) { + return nickname.replaceAll("\\s+", "").toLowerCase(Locale.ROOT); + } + + @Nullable + public String getNormalizedNickname() { + return normalizedNickname; + } + public MutableComponent getFullNickname() { return fullNickname; } @@ -500,10 +514,12 @@ public MutableComponent copyFullNickname() { } public int setNickname(Component nickname) { + String oldNormalizedNickname = this.normalizedNickname; int resultCode = 0; // Reset nickname if (nickname == null) { this.nickname = null; + this.normalizedNickname = null; resultCode = 1; EssentialCommands.LOGGER.info( "Cleared {}'s nickname", @@ -533,10 +549,11 @@ public int setNickname(Component nickname) { // Set nickname this.nickname = nickname; + this.normalizedNickname = normalizeNickname(nickname.getString()); } reloadFullNickname(); - PlayerDataManager.getInstance().markNicknameDirty(this); + PlayerDataManager.getInstance().markNicknameDirty(this, oldNormalizedNickname); this.setDirty(); // Return codes based on fail/success // ex: caused by profanity filter. diff --git a/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerDataManager.java b/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerDataManager.java index 3d342294..520295f0 100644 --- a/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerDataManager.java +++ b/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerDataManager.java @@ -39,6 +39,7 @@ public class PlayerDataManager { private final ConcurrentHashMap dataMap; + private final ConcurrentHashMap> dataByNicknameMap; private final List changedNicknames; private final List changedTeams; private final List nextTickTasks; @@ -50,6 +51,7 @@ public PlayerDataManager() { this.changedTeams = new ArrayList<>(); this.nextTickTasks = new ArrayList<>(); this.dataMap = new ConcurrentHashMap<>(); + this.dataByNicknameMap = new ConcurrentHashMap<>(); } public static void init() { @@ -101,8 +103,10 @@ public static PlayerDataManager getInstance() { return instance != null ? instance : new PlayerDataManager(); } - public void markNicknameDirty(PlayerData playerData) { + public void markNicknameDirty(PlayerData playerData, @Nullable String oldNormalizedNickname) { changedNicknames.add(playerData); + removeFromNicknameMap(playerData, oldNormalizedNickname); + addToNicknameMap(playerData); } public void markNicknameDirty(String playerName) { @@ -314,6 +318,7 @@ private PlayerData loadPlayerData(ServerPlayer player) { PlayerData playerData = ((ServerPlayerEntityAccess) player).ec$getPlayerData(); dataMap.put(player.getUUID(), playerData); + addToNicknameMap(playerData); return playerData; } @@ -323,7 +328,10 @@ PlayerData getPlayerDataFromUUID(UUID playerID) { // SAVE / LOAD private void unloadPlayerData(ServerPlayer player) { - this.dataMap.remove(player.getUUID()); + PlayerData playerData = this.dataMap.remove(player.getUUID()); + if (playerData != null) { + removeFromNicknameMap(playerData, playerData.getNormalizedNickname()); + } } public Collection getAllPlayerData() { @@ -335,19 +343,38 @@ public PlayerData getByUuid(UUID uuid) { return dataMap.get(uuid); } - /** - * Case insentitive - */ + private static String nicknameKey(String nickname) { + return PlayerData.normalizeNickname(nickname); + } + + private void addToNicknameMap(PlayerData playerData) { + String key = playerData.getNormalizedNickname(); + if (key != null && !key.isBlank()) { + dataByNicknameMap.compute(key, (k, list) -> { + if (list == null) list = new ArrayList<>(); + list.add(playerData); + return list; + }); + } + } + + // Key is passed explicitly because the caller captures it before mutation. + private void removeFromNicknameMap(PlayerData playerData, @Nullable String key) { + if (key != null) { + dataByNicknameMap.computeIfPresent(key, (k, list) -> { + list.remove(playerData); + return list.isEmpty() ? null : list; + }); + } + } + + // Case-insensitive, whitespace-insensitive lookup. + public List getByNickname(String nickname) { + List result = dataByNicknameMap.get(nicknameKey(nickname)); + return result != null ? List.copyOf(result) : List.of(); + } + public List getPlayerDataMatchingNickname(String nickname) { - return dataMap - .values() - .stream() - .filter(playerData -> - playerData - .getNickname() - .map(nick -> nick.getString().equalsIgnoreCase(nickname)) - .orElse(false) - ) - .collect(Collectors.toList()); + return getByNickname(nickname); } } From 4ddb1073999c8ed2b23c368379e11e0b931611ad Mon Sep 17 00:00:00 2001 From: John Paul R Date: Sun, 30 Aug 2026 15:09:45 -0400 Subject: [PATCH 4/8] mixin suggestions --- .../commands/NicknameTargetResolver.java | 40 ++++++++++--------- .../resources/essential_commands.mixins.json | 15 +++---- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/fibermc/essentialcommands/commands/NicknameTargetResolver.java b/src/main/java/com/fibermc/essentialcommands/commands/NicknameTargetResolver.java index f437a991..c3a63034 100644 --- a/src/main/java/com/fibermc/essentialcommands/commands/NicknameTargetResolver.java +++ b/src/main/java/com/fibermc/essentialcommands/commands/NicknameTargetResolver.java @@ -41,29 +41,33 @@ private static CompletableFuture suggestPlayers( CommandContext context, SuggestionsBuilder builder ) throws CommandSyntaxException { - if ( - CONFIG.NICKNAMES_AS_COMMAND_ARG != NicknameCommandArgMode.Never - && PlayerDataManager.exists() - ) { - String remaining = PlayerData.normalizeNickname(builder.getRemaining()); - - for (PlayerData playerData : PlayerDataManager.getInstance().getAllPlayerData()) { - String normalized = playerData.getNormalizedNickname(); - if (normalized == null || !normalized.startsWith(remaining)) { - continue; - } - // Suggest whitespace-stripped but case-preserved form - playerData.getNickname() - .map(Component::getString) - .map(nick -> nick.replaceAll("\\s+", "")) - .filter(nick -> !nick.isBlank()) - .ifPresent(builder::suggest); - } + if (CONFIG.NICKNAMES_AS_COMMAND_ARG != NicknameCommandArgMode.Never) { + addNicknameSuggestions(builder); } return EntityArgument.player().listSuggestions(context, builder); } + public static void addNicknameSuggestions(SuggestionsBuilder builder) { + if (!PlayerDataManager.exists()) { + return; + } + + String remaining = PlayerData.normalizeNickname(builder.getRemaining()); + + for (PlayerData playerData : PlayerDataManager.getInstance().getAllPlayerData()) { + String normalized = playerData.getNormalizedNickname(); + if (normalized == null || !normalized.startsWith(remaining)) { + continue; + } + playerData.getNickname() + .map(Component::getString) + .map(nick -> nick.replaceAll("\\s+", "")) + .filter(nick -> !nick.isBlank()) + .ifPresent(builder::suggest); + } + } + /** * Resolves an Essential Commands player argument. * diff --git a/src/main/resources/essential_commands.mixins.json b/src/main/resources/essential_commands.mixins.json index d7dbf312..50a6193e 100644 --- a/src/main/resources/essential_commands.mixins.json +++ b/src/main/resources/essential_commands.mixins.json @@ -3,19 +3,20 @@ "package": "com.fibermc.essentialcommands.mixin", "compatibilityLevel": "JAVA_17", "mixins": [ - "LivingEntityMixin", - "DimensionDataStorageInvoker", - "PlayerMixin", "ClientboundPlayerInfoUpdatePacketActionMixin", + "DimensionDataStorageInvoker", + "EntityArgumentNicknameSuggestionMixin", + "EntitySelectorNicknameMixin", + "LivingEntityMixin", + "PlayerDataStorageMixin", "PlayerListMixin", + "PlayerMixin", "PrepareSpawnTaskMixin", - "ServerPlayerMixin", "ServerGamePacketListenerImplMixin", + "ServerPlayerMixin", "ServerScoreboardMixin", "SleepStatusMixin", - "EntitySelectorNicknameMixin", - "TeleportCommandMixin", - "PlayerDataStorageMixin" + "TeleportCommandMixin" ], "client": [ ], From 550accd7ee316cd312b403165251b9905b5edbf1 Mon Sep 17 00:00:00 2001 From: John Paul R Date: Sun, 30 Aug 2026 16:17:32 -0400 Subject: [PATCH 5/8] afk does not change nickname suggestions --- .../fibermc/essentialcommands/playerdata/PlayerData.java | 2 +- .../essentialcommands/playerdata/PlayerDataManager.java | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerData.java b/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerData.java index cd184d6a..73d30b7d 100644 --- a/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerData.java +++ b/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerData.java @@ -334,7 +334,7 @@ public void setAfk(boolean afk) { } } - PlayerDataManager.getInstance().markNicknameDirty(this, this.normalizedNickname); + PlayerDataManager.getInstance().markDisplayNameDirty(this); } public boolean isAfk() { diff --git a/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerDataManager.java b/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerDataManager.java index 520295f0..534af6e0 100644 --- a/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerDataManager.java +++ b/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerDataManager.java @@ -103,8 +103,12 @@ public static PlayerDataManager getInstance() { return instance != null ? instance : new PlayerDataManager(); } - public void markNicknameDirty(PlayerData playerData, @Nullable String oldNormalizedNickname) { + public void markDisplayNameDirty(PlayerData playerData) { changedNicknames.add(playerData); + } + + public void markNicknameDirty(PlayerData playerData, @Nullable String oldNormalizedNickname) { + markDisplayNameDirty(playerData); removeFromNicknameMap(playerData, oldNormalizedNickname); addToNicknameMap(playerData); } From e410aac765822d54e592c9cda46b505798df15a8 Mon Sep 17 00:00:00 2001 From: John Paul R Date: Sun, 30 Aug 2026 16:18:11 -0400 Subject: [PATCH 6/8] track suggestions mixin --- ...EntityArgumentNicknameSuggestionMixin.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/main/java/com/fibermc/essentialcommands/mixin/EntityArgumentNicknameSuggestionMixin.java diff --git a/src/main/java/com/fibermc/essentialcommands/mixin/EntityArgumentNicknameSuggestionMixin.java b/src/main/java/com/fibermc/essentialcommands/mixin/EntityArgumentNicknameSuggestionMixin.java new file mode 100644 index 00000000..fb64e43b --- /dev/null +++ b/src/main/java/com/fibermc/essentialcommands/mixin/EntityArgumentNicknameSuggestionMixin.java @@ -0,0 +1,50 @@ +package com.fibermc.essentialcommands.mixin; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import com.fibermc.essentialcommands.commands.NicknameTargetResolver; +import com.fibermc.essentialcommands.types.NicknameCommandArgMode; +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 com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; + +import net.minecraft.commands.arguments.EntityArgument; + +import static com.fibermc.essentialcommands.EssentialCommands.CONFIG; + +/** + * Adds nickname suggestions to all EntityArgument-based commands + * (e.g. /tp, /give) when global nickname arguments are enabled. + */ +@Mixin(EntityArgument.class) +public class EntityArgumentNicknameSuggestionMixin { + + @Inject(method = "listSuggestions", at = @At("RETURN"), cancellable = true) + private void ec$addNicknameSuggestions( + CommandContext context, + SuggestionsBuilder builder, + CallbackInfoReturnable> cir + ) { + if (CONFIG.NICKNAMES_AS_COMMAND_ARG != NicknameCommandArgMode.Everywhere) { + return; + } + + SuggestionsBuilder nicknameBuilder = builder.createOffset(builder.getStart()); + NicknameTargetResolver.addNicknameSuggestions(nicknameBuilder); + Suggestions nicknameSuggestions = nicknameBuilder.build(); + + if (!nicknameSuggestions.getList().isEmpty()) { + cir.setReturnValue( + cir.getReturnValue().thenApply(vanilla -> + Suggestions.merge(builder.getInput(), List.of(vanilla, nicknameSuggestions)) + ) + ); + } + } +} From 97c4ab41c154b110b001bb2ef4ae6015b6ac4354 Mon Sep 17 00:00:00 2001 From: John Paul R Date: Sun, 30 Aug 2026 16:30:41 -0400 Subject: [PATCH 7/8] docs on enum values instead of fn --- .../commands/NicknameTargetResolver.java | 10 ---------- .../types/NicknameCommandArgMode.java | 7 +++++++ 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/fibermc/essentialcommands/commands/NicknameTargetResolver.java b/src/main/java/com/fibermc/essentialcommands/commands/NicknameTargetResolver.java index c3a63034..23340ca7 100644 --- a/src/main/java/com/fibermc/essentialcommands/commands/NicknameTargetResolver.java +++ b/src/main/java/com/fibermc/essentialcommands/commands/NicknameTargetResolver.java @@ -68,16 +68,6 @@ public static void addNicknameSuggestions(SuggestionsBuilder builder) { } } - /** - * Resolves an Essential Commands player argument. - * - * Everywhere: - * EntityArgument resolves nicknames through EntitySelectorNicknameMixin. - * EssentialCommandsOnly: - * Vanilla is attempted first, then the shared literal nickname fallback. - * Never: - * Vanilla behavior is used unchanged. - */ public static ServerPlayer getPlayer( CommandContext context, String argumentName diff --git a/src/main/java/com/fibermc/essentialcommands/types/NicknameCommandArgMode.java b/src/main/java/com/fibermc/essentialcommands/types/NicknameCommandArgMode.java index 8ea59753..9b727dd8 100644 --- a/src/main/java/com/fibermc/essentialcommands/types/NicknameCommandArgMode.java +++ b/src/main/java/com/fibermc/essentialcommands/types/NicknameCommandArgMode.java @@ -1,7 +1,14 @@ package com.fibermc.essentialcommands.types; +import com.fibermc.essentialcommands.mixin.EntityArgumentNicknameSuggestionMixin; +import com.fibermc.essentialcommands.mixin.EntitySelectorNicknameMixin; + public enum NicknameCommandArgMode { + /** Nicknames resolve ({@link EntitySelectorNicknameMixin}) and suggest + * ({@link EntityArgumentNicknameSuggestionMixin}) in all commands via mixin. */ Everywhere, + /** Nicknames resolve and suggest only in Essential Commands. */ EssentialCommandsOnly, + /** Vanilla behavior unchanged -- no nickname resolution. */ Never } From 7adab7f9c1857d87515e82dbc1d6e10d6428249d Mon Sep 17 00:00:00 2001 From: John Paul R Date: Sun, 30 Aug 2026 16:32:51 -0400 Subject: [PATCH 8/8] docs: document the new config option --- docs/Config-Documentation.md | 12 ++++++++++++ docs/Feature-Guide.md | 4 ++++ 2 files changed, 16 insertions(+) diff --git a/docs/Config-Documentation.md b/docs/Config-Documentation.md index 8f393c13..f630ac49 100644 --- a/docs/Config-Documentation.md +++ b/docs/Config-Documentation.md @@ -64,6 +64,7 @@ The config file can be found at `config/EssentialCommands.properties` | nickname_above_head | false | boolean | | nickname_max_length | 32 | integer | | nickname_prefix | {"text":"~","color":"red"} | MinecraftText | +| nicknames_as_command_arg | Never | NicknameCommandArgMode | | nicknames_in_player_list | true | boolean | | ops_bypass_teleport_rules | true | boolean | | persist_back_location | false | boolean | @@ -131,6 +132,17 @@ You can use a tellraw generator like [MinecraftJson](https://www.minecraftjson.c Examples: `"Alexandra"`, `{"text":"Alex","color":"green","bold":true}` +### `NicknameCommandArgMode` + +Controls whether player nicknames can be used in place of real usernames in +command arguments. + +Valid values: + +- `Never` - nicknames are not resolved in any command argument (default) +- `EssentialCommandsOnly` - nicknames resolve and suggest only in Essential Commands +- `Everywhere` - nicknames resolve and suggest in all commands (e.g. `/tp`, `/give`) + ### `RespawnCondition` Valid values: diff --git a/docs/Feature-Guide.md b/docs/Feature-Guide.md index 2898e31a..dd167eac 100644 --- a/docs/Feature-Guide.md +++ b/docs/Feature-Guide.md @@ -212,6 +212,10 @@ Customize player display names. - `nick_reveal_on_hover` - Show real name on nickname hover - Default: `true` - `nickname_above_head` - Show nickname above player's head - Default: `false` - `nicknames_in_player_list` - Show nicknames in tab list - Default: `true` +- `nicknames_as_command_arg` - Whether nicknames can be used as player arguments in commands - Default: `Never` + - `Never` - Nicknames are not resolved in any command argument + - `EssentialCommandsOnly` - Nicknames resolve and suggest only in Essential Commands + - `Everywhere` - Nicknames resolve and suggest in all commands (e.g. `/tp`, `/give`) ## Utility Commands