Skip to content

Commit bb6e42b

Browse files
ImTauTauJohn-Paul-R
authored
feat: command targeting for nicknamed players (#398)
## Summary Adds configurable nickname-aware player targeting while preserving vanilla `EntityArgument` behavior as the primary resolution path. This revision was updated based on review feedback so nickname resolution is an additive fallback rather than a replacement for vanilla player/selector handling. ## Configuration Adds: `nicknames_as_command_arg` with the following values: - `Everywhere` - Nickname fallback is enabled for `EntityArgument`-based targeting globally. - Vanilla resolution runs first and is left unchanged if it returns a result. - `EssentialCommandsOnly` - Vanilla and other mod commands retain normal Minecraft behavior. - Essential Commands player arguments may fall back to nickname resolution. - `Never` - Nickname command argument support is disabled and vanilla behavior is used. The current default is `Never`. ## Nickname behavior - Real usernames retain precedence because vanilla resolution is always attempted first. - Whitespace is ignored for nickname command matching, e.g. `John Smith` can be addressed as `JohnSmith`. - If multiple online players normalize to the same nickname, no arbitrary match is selected. - The Essential Commands-only path and global mixin path share the same nickname resolution logic. ## Validation The revised implementation builds successfully with Gradle. The original nickname-targeting implementation was live-tested on Fabric 26.2, but this latest review-driven refactor has not yet been live-server tested and is being submitted for review/testing. --------- Co-authored-by: Tau <tau_assassin@hotmail.com> Co-authored-by: John Paul R <jp@jpcode.dev>
1 parent fb86fb7 commit bb6e42b

19 files changed

Lines changed: 361 additions & 45 deletions

docs/Config-Documentation.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ The config file can be found at `config/EssentialCommands.properties`
6464
| nickname_above_head | false | boolean |
6565
| nickname_max_length | 32 | integer |
6666
| nickname_prefix | {"text":"~","color":"red"} | MinecraftText |
67+
| nicknames_as_command_arg | Never | NicknameCommandArgMode |
6768
| nicknames_in_player_list | true | boolean |
6869
| ops_bypass_teleport_rules | true | boolean |
6970
| persist_back_location | false | boolean |
@@ -131,6 +132,17 @@ You can use a tellraw generator like [MinecraftJson](https://www.minecraftjson.c
131132

132133
Examples: `"Alexandra"`, `{"text":"Alex","color":"green","bold":true}`
133134

135+
### `NicknameCommandArgMode`
136+
137+
Controls whether player nicknames can be used in place of real usernames in
138+
command arguments.
139+
140+
Valid values:
141+
142+
- `Never` - nicknames are not resolved in any command argument (default)
143+
- `EssentialCommandsOnly` - nicknames resolve and suggest only in Essential Commands
144+
- `Everywhere` - nicknames resolve and suggest in all commands (e.g. `/tp`, `/give`)
145+
134146
### `RespawnCondition`
135147

136148
Valid values:

docs/Feature-Guide.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,10 @@ Customize player display names.
212212
- `nick_reveal_on_hover` - Show real name on nickname hover - Default: `true`
213213
- `nickname_above_head` - Show nickname above player's head - Default: `false`
214214
- `nicknames_in_player_list` - Show nicknames in tab list - Default: `true`
215+
- `nicknames_as_command_arg` - Whether nicknames can be used as player arguments in commands - Default: `Never`
216+
- `Never` - Nicknames are not resolved in any command argument
217+
- `EssentialCommandsOnly` - Nicknames resolve and suggest only in Essential Commands
218+
- `Everywhere` - Nicknames resolve and suggest in all commands (e.g. `/tp`, `/give`)
215219

216220
## Utility Commands
217221

src/main/java/com/fibermc/essentialcommands/EssentialCommandRegistry.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
import net.minecraft.commands.CommandSourceStack;
3333
import net.minecraft.commands.Commands;
3434
import net.minecraft.commands.arguments.ComponentArgument;
35-
import net.minecraft.commands.arguments.EntityArgument;
3635
import net.minecraft.network.chat.Component;
3736
import net.minecraft.server.permissions.Permission;
3837
import net.minecraft.server.permissions.PermissionLevel;
@@ -84,7 +83,7 @@ public static void register(
8483
if (CONFIG.ENABLE_TPA) {
8584
registerNode.accept(Commands.literal("tpa")
8685
.requires(ECPerms.require(ECPerms.Registry.tpa, 0))
87-
.then(CommandUtil.targetPlayerArgument()
86+
.then(NicknameTargetResolver.targetPlayerArgument()
8887
.executes(new TeleportAskCommand()))
8988
.build());
9089

@@ -96,22 +95,22 @@ public static void register(
9695
registerNode.accept(Commands.literal("tpaccept")
9796
.requires(ECPerms.require(ECPerms.Registry.tpaccept, 0))
9897
.executes(new TeleportAcceptCommand()::runDefault)
99-
.then(CommandUtil.targetPlayerArgument()
98+
.then(NicknameTargetResolver.targetPlayerArgument()
10099
.suggests(TeleportResponseSuggestion.STRING_SUGGESTIONS_PROVIDER)
101100
.executes(new TeleportAcceptCommand()))
102101
.build());
103102

104103
registerNode.accept(Commands.literal("tpdeny")
105104
.requires(ECPerms.require(ECPerms.Registry.tpdeny, 0))
106105
.executes(new TeleportDenyCommand()::runDefault)
107-
.then(CommandUtil.targetPlayerArgument()
106+
.then(NicknameTargetResolver.targetPlayerArgument()
108107
.suggests(TeleportResponseSuggestion.STRING_SUGGESTIONS_PROVIDER)
109108
.executes(new TeleportDenyCommand()))
110109
.build());
111110

112111
registerNode.accept(Commands.literal("tpahere")
113112
.requires(ECPerms.require(ECPerms.Registry.tpahere, 0))
114-
.then(CommandUtil.targetPlayerArgument()
113+
.then(NicknameTargetResolver.targetPlayerArgument()
115114
.executes(new TeleportAskHereCommand()))
116115
.build());
117116
}
@@ -148,7 +147,7 @@ public static void register(
148147

149148
homeTpOtherBuilder
150149
.requires(ECPerms.require(ECPerms.Registry.home_tp_others, 2))
151-
.then(argument("target_player", EntityArgument.player())
150+
.then(CommandUtil.targetPlayerArgument()
152151
.then(argument("home_name", StringArgumentType.word())
153152
.suggests(HomeTeleportOtherCommand.Suggestion.LIST_SUGGESTION_PROVIDER)
154153
.executes(new HomeTeleportOtherCommand())));
@@ -241,7 +240,7 @@ public static void register(
241240

242241
warpTpOtherBuilder
243242
.requires(ECPerms.require(ECPerms.Registry.home_tp_others, 2))
244-
.then(argument("target_player", EntityArgument.player())
243+
.then(CommandUtil.targetPlayerArgument()
245244
.then(argument("warp_name", StringArgumentType.word())
246245
.suggests(WarpSuggestion.STRING_SUGGESTIONS_PROVIDER)
247246
.executes(new WarpTpCommand()::runOther)));
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.fibermc.essentialcommands.access;
2+
3+
public interface EntitySelectorNicknameAccess {
4+
String ec$getPlayerName();
5+
}

src/main/java/com/fibermc/essentialcommands/commands/CommandUtil.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@
77
import com.mojang.brigadier.exceptions.CommandSyntaxException;
88
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
99
import com.mojang.brigadier.tree.CommandNode;
10-
1110
import net.minecraft.commands.CommandSourceStack;
12-
import net.minecraft.commands.Commands;
13-
import net.minecraft.commands.arguments.EntityArgument;
1411
import net.minecraft.commands.arguments.selector.EntitySelector;
1512
import net.minecraft.server.level.ServerPlayer;
1613

@@ -21,7 +18,7 @@ public final class CommandUtil {
2118
private CommandUtil() {}
2219

2320
public static RequiredArgumentBuilder<CommandSourceStack, EntitySelector> targetPlayerArgument() {
24-
return Commands.argument("target_player", EntityArgument.player());
21+
return NicknameTargetResolver.targetPlayerArgumentNonGreedy();
2522
}
2623

2724
public static String getCommandString(CommandSourceStack source, CommandNode<CommandSourceStack> commandNode) {
@@ -39,7 +36,7 @@ public static CommandSyntaxException createSimpleException(Message msg) {
3936

4037
public static ServerPlayer getCommandTargetPlayer(CommandContext<CommandSourceStack> context) throws CommandSyntaxException {
4138
try {
42-
return EntityArgument.getPlayer(context, "target_player");
39+
return NicknameTargetResolver.getPlayer(context, "target_player");
4340
} catch (IllegalArgumentException e) {
4441
return context.getSource().getPlayer();
4542
}

src/main/java/com/fibermc/essentialcommands/commands/HomeTeleportOtherCommand.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import com.mojang.brigadier.suggestion.SuggestionProvider;
2222

2323
import net.minecraft.commands.CommandSourceStack;
24-
import net.minecraft.commands.arguments.EntityArgument;
2524
import net.minecraft.network.chat.Component;
2625

2726
import static com.fibermc.essentialcommands.EssentialCommands.CONFIG;
@@ -36,7 +35,7 @@ public int run(CommandContext<CommandSourceStack> context) throws CommandSyntaxE
3635
}
3736

3837
private static PlayerData getTargetPlayerData(CommandContext<CommandSourceStack> context) throws CommandSyntaxException {
39-
return ((ServerPlayerEntityAccess) EntityArgument.getPlayer(context, "target_player")).ec$getPlayerData();
38+
return ((ServerPlayerEntityAccess) NicknameTargetResolver.getPlayer(context, "target_player")).ec$getPlayerData();
4039
}
4140

4241
public int runDefault(CommandContext<CommandSourceStack> context) throws CommandSyntaxException {
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package com.fibermc.essentialcommands.commands;
2+
3+
import java.util.List;
4+
import java.util.concurrent.CompletableFuture;
5+
6+
import com.fibermc.essentialcommands.access.EntitySelectorNicknameAccess;
7+
import com.fibermc.essentialcommands.playerdata.PlayerData;
8+
import com.fibermc.essentialcommands.playerdata.PlayerDataManager;
9+
import com.fibermc.essentialcommands.types.NicknameCommandArgMode;
10+
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
11+
import com.mojang.brigadier.context.CommandContext;
12+
import com.mojang.brigadier.exceptions.CommandSyntaxException;
13+
import com.mojang.brigadier.suggestion.Suggestions;
14+
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
15+
import net.minecraft.commands.CommandSourceStack;
16+
import net.minecraft.commands.Commands;
17+
import net.minecraft.commands.arguments.EntityArgument;
18+
import net.minecraft.commands.arguments.selector.EntitySelector;
19+
import net.minecraft.network.chat.Component;
20+
import net.minecraft.server.level.ServerPlayer;
21+
22+
import static com.fibermc.essentialcommands.EssentialCommands.CONFIG;
23+
24+
/**
25+
* Adds optional nickname fallback on top of Minecraft's normal player
26+
* argument handling. Vanilla resolution is always attempted first.
27+
*/
28+
public final class NicknameTargetResolver {
29+
private NicknameTargetResolver() {}
30+
31+
public static RequiredArgumentBuilder<CommandSourceStack, EntitySelector> targetPlayerArgument() {
32+
return Commands.argument("target_player", EntityArgument.player())
33+
.suggests(NicknameTargetResolver::suggestPlayers);
34+
}
35+
36+
public static RequiredArgumentBuilder<CommandSourceStack, EntitySelector> targetPlayerArgumentNonGreedy() {
37+
return targetPlayerArgument();
38+
}
39+
40+
private static CompletableFuture<Suggestions> suggestPlayers(
41+
CommandContext<CommandSourceStack> context,
42+
SuggestionsBuilder builder
43+
) throws CommandSyntaxException {
44+
if (CONFIG.NICKNAMES_AS_COMMAND_ARG != NicknameCommandArgMode.Never) {
45+
addNicknameSuggestions(builder);
46+
}
47+
48+
return EntityArgument.player().listSuggestions(context, builder);
49+
}
50+
51+
public static void addNicknameSuggestions(SuggestionsBuilder builder) {
52+
if (!PlayerDataManager.exists()) {
53+
return;
54+
}
55+
56+
String remaining = PlayerData.normalizeNickname(builder.getRemaining());
57+
58+
for (PlayerData playerData : PlayerDataManager.getInstance().getAllPlayerData()) {
59+
String normalized = playerData.getNormalizedNickname();
60+
if (normalized == null || !normalized.startsWith(remaining)) {
61+
continue;
62+
}
63+
playerData.getNickname()
64+
.map(Component::getString)
65+
.map(nick -> nick.replaceAll("\\s+", ""))
66+
.filter(nick -> !nick.isBlank())
67+
.ifPresent(builder::suggest);
68+
}
69+
}
70+
71+
public static ServerPlayer getPlayer(
72+
CommandContext<CommandSourceStack> context,
73+
String argumentName
74+
) throws CommandSyntaxException {
75+
try {
76+
return EntityArgument.getPlayer(context, argumentName);
77+
} catch (CommandSyntaxException vanillaFailure) {
78+
if (CONFIG.NICKNAMES_AS_COMMAND_ARG != NicknameCommandArgMode.EssentialCommandsOnly) {
79+
throw vanillaFailure;
80+
}
81+
82+
EntitySelector selector = context.getArgument(argumentName, EntitySelector.class);
83+
String playerName = ((EntitySelectorNicknameAccess) selector).ec$getPlayerName();
84+
ServerPlayer nicknameMatch = resolvePlayerByNickname(playerName);
85+
if (nicknameMatch != null) {
86+
return nicknameMatch;
87+
}
88+
89+
throw vanillaFailure;
90+
}
91+
}
92+
93+
/**
94+
* Finds one online player whose nickname matches the literal command
95+
* player name after normalization (whitespace removal + case folding).
96+
*
97+
* Returns null for no match or an ambiguous match.
98+
*/
99+
public static ServerPlayer resolvePlayerByNickname(String playerName) {
100+
if (
101+
playerName == null
102+
|| playerName.isBlank()
103+
) {
104+
return null;
105+
}
106+
107+
List<PlayerData> matches = PlayerDataManager.getInstance().getByNickname(playerName);
108+
// Ambiguous (>1) or no match -- never choose one arbitrarily.
109+
if (matches.size() != 1) {
110+
return null;
111+
}
112+
return matches.getFirst().getPlayer();
113+
}
114+
}

src/main/java/com/fibermc/essentialcommands/commands/TeleportAskCommand.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import com.mojang.brigadier.exceptions.CommandSyntaxException;
1515

1616
import net.minecraft.commands.CommandSourceStack;
17-
import net.minecraft.commands.arguments.EntityArgument;
1817
import net.minecraft.server.level.ServerPlayer;
1918

2019
public class TeleportAskCommand implements Command<CommandSourceStack> {
@@ -25,7 +24,7 @@ public TeleportAskCommand() {}
2524
public int run(CommandContext<CommandSourceStack> context) throws CommandSyntaxException {
2625
TeleportManager tpMgr = ManagerLocator.getInstance().getTpManager();
2726
ServerPlayer senderPlayer = context.getSource().getPlayerOrException();
28-
ServerPlayer targetPlayer = EntityArgument.getPlayer(context, "target_player");
27+
ServerPlayer targetPlayer = NicknameTargetResolver.getPlayer(context, "target_player");
2928
var senderPlayerData = PlayerData.access(senderPlayer);
3029
var targetPlayerData = PlayerData.access(targetPlayer);
3130

src/main/java/com/fibermc/essentialcommands/commands/TeleportAskHereCommand.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import com.mojang.brigadier.exceptions.CommandSyntaxException;
1313

1414
import net.minecraft.commands.CommandSourceStack;
15-
import net.minecraft.commands.arguments.EntityArgument;
1615
import net.minecraft.server.level.ServerPlayer;
1716

1817
public class TeleportAskHereCommand implements Command<CommandSourceStack> {
@@ -23,7 +22,7 @@ public TeleportAskHereCommand() {}
2322
public int run(CommandContext<CommandSourceStack> context) throws CommandSyntaxException {
2423
TeleportManager tpMgr = ManagerLocator.getInstance().getTpManager();
2524
ServerPlayer senderPlayer = context.getSource().getPlayerOrException();
26-
ServerPlayer targetPlayer = EntityArgument.getPlayer(context, "target_player");
25+
ServerPlayer targetPlayer = NicknameTargetResolver.getPlayer(context, "target_player");
2726
var senderPlayerData = PlayerData.access(senderPlayer);
2827
var targetPlayerData = PlayerData.access(targetPlayer);
2928

src/main/java/com/fibermc/essentialcommands/commands/TeleportResponseCommand.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import com.mojang.brigadier.exceptions.CommandSyntaxException;
1414

1515
import net.minecraft.commands.CommandSourceStack;
16-
import net.minecraft.commands.arguments.EntityArgument;
1716
import net.minecraft.server.level.ServerPlayer;
1817

1918
public abstract class TeleportResponseCommand implements Command<CommandSourceStack> {
@@ -22,7 +21,7 @@ public int run(CommandContext<CommandSourceStack> context) throws CommandSyntaxE
2221
return exec(
2322
context,
2423
context.getSource().getPlayer(),
25-
EntityArgument.getPlayer(context, "target_player")
24+
NicknameTargetResolver.getPlayer(context, "target_player")
2625
);
2726
}
2827

0 commit comments

Comments
 (0)