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 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/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 99f67f75..a931acad 100644 --- a/src/main/java/com/fibermc/essentialcommands/commands/CommandUtil.java +++ b/src/main/java/com/fibermc/essentialcommands/commands/CommandUtil.java @@ -7,10 +7,7 @@ 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.Commands; -import net.minecraft.commands.arguments.EntityArgument; import net.minecraft.commands.arguments.selector.EntitySelector; import net.minecraft.server.level.ServerPlayer; @@ -21,7 +18,7 @@ public final class CommandUtil { private CommandUtil() {} public static RequiredArgumentBuilder targetPlayerArgument() { - return Commands.argument("target_player", EntityArgument.player()); + 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..23340ca7 --- /dev/null +++ b/src/main/java/com/fibermc/essentialcommands/commands/NicknameTargetResolver.java @@ -0,0 +1,114 @@ +package com.fibermc.essentialcommands.commands; + +import java.util.List; +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.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; +import net.minecraft.commands.arguments.selector.EntitySelector; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; + +import static com.fibermc.essentialcommands.EssentialCommands.CONFIG; + +/** + * 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() {} + + public static RequiredArgumentBuilder targetPlayerArgument() { + return Commands.argument("target_player", EntityArgument.player()) + .suggests(NicknameTargetResolver::suggestPlayers); + } + + public static RequiredArgumentBuilder targetPlayerArgumentNonGreedy() { + return targetPlayerArgument(); + } + + private static CompletableFuture suggestPlayers( + CommandContext context, + SuggestionsBuilder builder + ) throws CommandSyntaxException { + 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); + } + } + + public static ServerPlayer getPlayer( + CommandContext context, + String argumentName + ) throws CommandSyntaxException { + 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) selector).ec$getPlayerName(); + ServerPlayer nicknameMatch = resolvePlayerByNickname(playerName); + if (nicknameMatch != null) { + return nicknameMatch; + } + + throw vanillaFailure; + } + } + + /** + * Finds one online player whose nickname matches the literal command + * player name after normalization (whitespace removal + case folding). + * + * Returns null for no match or an ambiguous match. + */ + public static ServerPlayer resolvePlayerByNickname(String playerName) { + if ( + playerName == null + || playerName.isBlank() + ) { + return null; + } + + List matches = PlayerDataManager.getInstance().getByNickname(playerName); + // Ambiguous (>1) or no match -- never choose one arbitrarily. + if (matches.size() != 1) { + return null; + } + return matches.getFirst().getPlayer(); + } +} 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/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/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)) + ) + ); + } + } +} 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..c379962c --- /dev/null +++ b/src/main/java/com/fibermc/essentialcommands/mixin/EntitySelectorNicknameMixin.java @@ -0,0 +1,71 @@ +package com.fibermc.essentialcommands.mixin; + +import java.util.List; + +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 nickname fallback after vanilla EntitySelector resolution when global + * nickname command arguments are enabled. + */ +@Mixin(EntitySelector.class) +public abstract class EntitySelectorNicknameMixin implements EntitySelectorNicknameAccess { + @Shadow + @Final + private String playerName; + + @Override + public String ec$getPlayerName() { + return this.playerName; + } + + @Inject(method = "findPlayers", at = @At("RETURN"), cancellable = true) + private void ec$resolveNicknameForPlayers( + CommandSourceStack source, + CallbackInfoReturnable> cir + ) { + if ( + CONFIG.NICKNAMES_AS_COMMAND_ARG != NicknameCommandArgMode.Everywhere + || !cir.getReturnValue().isEmpty() + ) { + return; + } + + ServerPlayer player = NicknameTargetResolver.resolvePlayerByNickname(this.playerName); + if (player != null) { + cir.setReturnValue(List.of(player)); + } + } + + @Inject(method = "findEntities", at = @At("RETURN"), cancellable = true) + private void ec$resolveNicknameForEntities( + CommandSourceStack source, + CallbackInfoReturnable> cir + ) { + if ( + CONFIG.NICKNAMES_AS_COMMAND_ARG != NicknameCommandArgMode.Everywhere + || !cir.getReturnValue().isEmpty() + ) { + return; + } + + 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..73d30b7d 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().markDisplayNameDirty(this); } 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..534af6e0 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,10 +103,16 @@ public static PlayerDataManager getInstance() { return instance != null ? instance : new PlayerDataManager(); } - public void markNicknameDirty(PlayerData playerData) { + public void markDisplayNameDirty(PlayerData playerData) { changedNicknames.add(playerData); } + public void markNicknameDirty(PlayerData playerData, @Nullable String oldNormalizedNickname) { + markDisplayNameDirty(playerData); + removeFromNicknameMap(playerData, oldNormalizedNickname); + addToNicknameMap(playerData); + } + public void markNicknameDirty(String playerName) { changedTeams.add(playerName); } @@ -314,6 +322,7 @@ private PlayerData loadPlayerData(ServerPlayer player) { PlayerData playerData = ((ServerPlayerEntityAccess) player).ec$getPlayerData(); dataMap.put(player.getUUID(), playerData); + addToNicknameMap(playerData); return playerData; } @@ -323,7 +332,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 +347,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); } } 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..9b727dd8 --- /dev/null +++ b/src/main/java/com/fibermc/essentialcommands/types/NicknameCommandArgMode.java @@ -0,0 +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 +} diff --git a/src/main/resources/essential_commands.mixins.json b/src/main/resources/essential_commands.mixins.json index 21d014cc..50a6193e 100644 --- a/src/main/resources/essential_commands.mixins.json +++ b/src/main/resources/essential_commands.mixins.json @@ -3,18 +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", - "TeleportCommandMixin", - "PlayerDataStorageMixin" + "TeleportCommandMixin" ], "client": [ ],