Skip to content

feat: command targeting for nicknamed players - #398

Open
ImTau wants to merge 2 commits into
John-Paul-R:26.xfrom
ImTau:fix/nickname-player-targeting
Open

feat: command targeting for nicknamed players#398
ImTau wants to merge 2 commits into
John-Paul-R:26.xfrom
ImTau:fix/nickname-player-targeting

Conversation

@ImTau

@ImTau ImTau commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

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.

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.
@John-Paul-R

Copy link
Copy Markdown
Owner

Hi, thanks for working to make EC better.

So, I've avoided doing something like this attempts for two reasons so far:

  1. I don't want to change the way EC processes playername arguments relative to the base game
  2. I worry about wholesale replacing vanilla argument parsing behavior, as this could break other features, especially as MC gets updates

but I will review this implementation to see how/if it addresses the above. (Or maybe if its something that could be toggled in the config.)

@ImTau

ImTau commented Aug 29, 2026

Copy link
Copy Markdown
Contributor Author

Hi, thanks for working to make EC better.

So, I've avoided doing something like this attempts for two reasons so far:

  1. I don't want to change the way EC processes playername arguments relative to the base game
  2. I worry about wholesale replacing vanilla argument parsing behavior, as this could break other features, especially as MC gets updates

but I will review this implementation to see how/if it addresses the above. (Or maybe if its something that could be toggled in the config.)

Yeah, no worries! That makes sense. I just wanted a fix for my needs mostly. I know coming from EssentialsX a lot of players enjoy the nicknames. I'll try to get my conversion tool fix for the EssentialsX homes posted and you can check it out. Might be less sensitive since it's a niche use case.

@John-Paul-R John-Paul-R changed the title Fix command targeting for nicknamed players feat: command targeting for nicknamed players Aug 29, 2026
Comment on lines +95 to +133
// 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 + "\"."
));

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have a separate argument type for nickname-players? doesn't the EntitySelectorNicknameMixin add nickname
support to findPlayers and findEntities, used by EntityArgument?

Does this one have different behavior?

@ImTau ImTau Aug 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that there is overlap. The separate resolver was added first for EC commands. The EntitySelector mixin was added afterward to cover vanilla /tp. With the mixin in place, ordinary single-token nickname lookup through EntityArgument.player() should already work.

The remaining differences inside the custom argument are that it supports nicknames containing spaces, adds nickname-aware suggestions and gives you explicit ambiguity/not-found errors. The mixin only affects command resolution in findPlayers/findEntities.

If preserving vanilla EntityArgument semantics is preferable. I'd think the cleaner implementation would be to use EntityArgument.player() everywhere and let the mixin handle lookup, unless support for spaced nicknames/suggestions is something you'd specifically want to retain.

@John-Paul-R John-Paul-R Aug 29, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I think that the nicknames-with-spaces support is nice, but we could also achieve it by stripping whitespace from within the nickname, which I think could work inside EntityArgument. (We are already stripping some forms of disallowed char via getString presumably (like MC's random-text thing)).

Ultimately, I think this feature is best behind a config option, that allows players to choose how "on" this special argument parsing is. I'm thinking something like nicknames_as_command_arg with as an enum config option with possible options of Everywhere, EssentialCommandsOnly, or Never.

That could be enabled by just eary-return-ing in the mixins when it is EssentialCommandsOnly or Never. And by choosing between the vanilla parser and the custom ec$resolveLiteralPlayer in the NicknameTargetResolver for the "essential commands" bit (that way both use the same implementation).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense. I agree on the three-state config approach as well as the resolver should be shared rather than having the mixin and NicknameTargetResolver maintain separate lookup behavior.

Let me change it up so Everywhere enables the EntitySelector mixin fallback globally, EssentialCommandsOnly keeps the mixin out of vanilla commands but uses the shared nickname resolver for EC arguments and never stays entirely vanilla.

For whitespace normalization I'll preserve ambiguity handling, since e.g. Foo Bar and FooBar could otherwise collapse to the same command-form nickname. Which might cause problems.

I'll update the PR.

Comment on lines +31 to +40
@Inject(method = "findPlayers", at = @At("HEAD"), cancellable = true)
private void ec$resolveNicknameForPlayers(
CommandSourceStack source,
CallbackInfoReturnable<List<ServerPlayer>> cir
) {
ServerPlayer player = ec$resolveLiteralPlayer(source);
if (player != null) {
cir.setReturnValue(Collections.singletonList(player));
}
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The vanilla findPlayers really does a lot more internally than we consider below in $ec$resolveLiteralPlayer. Ideally, we should just be adding onto the existing vanilla results when this is enabled, rather than replacing them, in order to avoid breaking expectations that other in-game systems might have.

This would also allow us to avoid trying to replicate the vanilla behavior like this (we'd just be concerned with our nickname-specific additions):

        // Preserve vanilla username precedence.
        ServerPlayer usernameMatch = source
            .getServer()
            .getPlayerList()
            .getPlayerByName(this.playerName);
        if (usernameMatch != null) {
            return usernameMatch;
        }

Vanilla EntitySelector.findPlayer:

    public List<ServerPlayer> findPlayers(final CommandSourceStack sender) throws CommandSyntaxException {
        this.checkPermissions(sender);
        if (this.playerName != null) {
            ServerPlayer result = sender.getServer().getPlayerList().getPlayerByName(this.playerName);
            return result == null ? List.of() : List.of(result);
        } else if (this.entityUUID != null) {
            ServerPlayer result = sender.getServer().getPlayerList().getPlayer(this.entityUUID);
            return result == null ? List.of() : List.of(result);
        } else {
            Vec3 pos = (Vec3)this.position.apply(sender.getPosition());
            AABB absoluteAabb = this.getAbsoluteAabb(pos);
            Predicate<Entity> predicate = this.getPredicate(pos, absoluteAabb, (FeatureFlagSet)null);
            if (this.currentEntity) {
                Entity var12 = sender.getEntity();
                if (var12 instanceof ServerPlayer) {
                    ServerPlayer player = (ServerPlayer)var12;
                    if (predicate.test(player)) {
                        return List.of(player);
                    }
                }

                return List.of();
            } else {
                int limit = this.getResultLimit();
                List<ServerPlayer> result;
                if (this.isWorldLimited()) {
                    result = sender.getLevel().getPlayers(predicate, limit);
                } else {
                    result = new ObjectArrayList();

                    for(ServerPlayer player : sender.getServer().getPlayerList().getPlayers()) {
                        if (predicate.test(player)) {
                            result.add(player);
                            if (result.size() >= limit) {
                                return result;
                            }
                        }
                    }
                }

                return this.<ServerPlayer>sortAndLimit(pos, result);
            }
        }
    }

@John-Paul-R John-Paul-R Aug 29, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(we might be able to do this by intercepting the vanilla return value, instead of injecting at HEAD, I think?)

Comment on lines +125 to +133
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 + "\"."
));

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should use translations when making error messages. See an example here:

public static void exec(CommandSourceStack source, ServerPlayer target, int flySpeed) throws CommandSyntaxException {
ECText ecTextTarget = ECText.access(target);
if (flySpeed > CONFIG.FLY_MAX_SPEED)
throw CommandUtil.createSimpleException(
ecTextTarget.getText(
"cmd.fly.speed.error.limit",
TextFormatType.Error,
ecTextTarget.accent(String.valueOf(CONFIG.FLY_MAX_SPEED))
));
int oldFlySpeed = (int)(target.getAbilities().getFlyingSpeed() * speedMultiplier);
target.getAbilities().setFlyingSpeed((float)flySpeed / speedMultiplier);
target.onUpdateAbilities();
if (!Objects.equals(source.getPlayer(), target)) {
ECText ecTextSource = ECText.access(source.getPlayer());
source.sendSuccess(() ->
ecTextSource.getText(
"cmd.fly.speed.feedback.update.other",
ecTextSource.accent(String.valueOf(oldFlySpeed)),
ecTextSource.accent(String.valueOf(flySpeed)),
target.getDisplayName()
),
CONFIG.BROADCAST_TO_OPS
);
}

and fill in the english translations:

https://github.com/John-Paul-R/Essential-Commands/blob/3190a17b42f17a32c5487afa42b3e6a609c77ed9/src/main/resources/data/essential_commands/lang/en_us.json#L1-L0

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I pushed a revision based on the feedback. The mixin now runs after vanilla resolution and only adds a nickname result when vanilla found nothing. I also added the three-state nicknames_as_command_arg config and shared the nickname fallback between global and EC-only handling. Whitespace normalization is included with ambiguity protection. The revised code builds successfully with Gradle; I have not live-server tested this latest iteration tho.

Add configurable nickname argument scope and use vanilla-first nickname fallback with shared resolution logic.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants