Skip to content

Instantly share code, notes, and snippets.

@tryashtar
Last active September 10, 2024 12:38
Show Gist options
  • Save tryashtar/0c3adfddbf885f581476fee0d524cef3 to your computer and use it in GitHub Desktop.
Save tryashtar/0c3adfddbf885f581476fee0d524cef3 to your computer and use it in GitHub Desktop.
Code analysis of target selectors

Requirements

Selector arguments can optionally have two requirements: requiring only players, and requiring only a single target. Here are some examples of arguments of each type:

  • Only players: /give <targets>
  • Only a single target: /damage <target>
  • Only a single player: /xp query <targets>
  • Any number of any entities: /kill <targets>

As is well-known, each selector type has default "implicit" arguments. All behavior below will be described in terms of these arguments, not the base selectors themselves.

  • @p, @a, and @r have an implicit type=player, and will fail to parse if given a different type.
  • @p, @r, and @n have an implicit limit=1, which can be modified.
  • @p and @n have an implicit sort=nearest, @r has an implicit sort=random, and @a and @e have an implicit sort=arbitrary, all of which can be modified.
  • @s will fail to parse if given limit.

To fulfill the "only players" requirement, the selector must be any of the following:

  • A literal player name
  • Any selector with a type=player argument
  • Any selector with any level, gamemode, or advancements argument
  • For the sake of parsing (but not execution), a base selector of @s is sufficient

Note that @s is always accepted, even with a non-player type, and UUIDs are not accepted.

To fulfill the "only a single target" requirement, the selector must be any of the following:

  • A literal player name
  • A UUID
  • A base selector of @s
  • Any selector with a limit=1 argument

Lastly, a selector is "dimension-limited" if it contains any distance, x, y, z, dx, dy, or dz argument. As explained below, this affects where the server looks for results. It doesn't apply to @s.

Execution

When a selector with arguments is used, an axis-aligned bounding box can be generated. If any of dx, dy, or dz are present, they form the box. If only distance is present and has an upper bound, a box with radius equal to that bound is used.

A predicate is also created. It checks, in order:

  • If the base selector is @e or @n, the entity must be alive (not dying)
  • The name, gamemode, team, type, tag, nbt, scores, advancements, and predicate arguments in the order they were written in the selector
  • The x_rotation and y_rotation arguments
  • The level argument
  • The entity's bounding box must intersect with the generated AABB, if one exists
  • The entity's distance to the context position must be within distance, if specified

Regardless of whether the argument required it, if the above conditions for "only players" are met, the game enters a fast path for players.

  • For a literal player name, the game does a simple linear search on the list of online players to find a case-insensitive profile name match.
  • For an @s selector, the game checks if the context entity is a player and matches the selector predicate.
  • For a dimension-limited selector, the game iterates the context dimension's player list, otherwise it iterates the server's list of online players.
    • The game adds each player that matches the selector predicate.
    • If sort=arbitrary, the game will stop early after adding limit players.
    • Afterwards, if sort is not arbitrary, the list is sorted, and additional players above limit are discarded.

If not, the game falls back to the default entity search.

  • For a UUID, the game looks through each dimension, performing hashmap lookups on the UUID to find the matching loaded entity.
  • For an @s selector, the game checks if the context entity matches the selector predicate.
  • For a dimension-limited selector, the game uses the context's dimension, otherwise it iterates through all dimensions. Here it gets a bit hairy.
    • If an AABB was generated, the game makes an effort to only search chunk sections that intersect with it. The relevant code is EntitySectionStorage.forEachAccessibleNonEmptySection; the details are somewhat opaque to me.
    • Although each section is capable of retrieving its entities by a specific entity type, it doesn't appear to do so. For reference, this is ClassInstanceMultiMap.find, which creates or searches a cached view of entities that derive from the passed-in class. In this case, that's EntityTypeTest.getBaseClass from the type argument, but EntityType appears to always return Entity.class, so this functionality is not available to commands; all entities are returned and only filtered out by tryCast. Please let me know if I'm misunderstanding any parts here.
    • If an AABB was not generated, the game iterates through all loaded entities in the UUID map.
    • If there is a non-inverted non-tag type argument, entities must match that type.
    • Entities' bounding boxes must intersect with the generated AABB, if there was one.
    • Entities must match the predicate.
    • If sort=arbitrary, the game will stop early immediately after adding limit entities.
    • Afterwards, if sort is not arbitrary, the list is sorted, and additional entities above limit are discarded.

Takeaways

This code analysis does not substitute for specific, accurate performance measurements on your particular project. However, there are some obvious takeaways that should be remembered:

  • limit is very useful when sort=arbitrary, and completely ineffectual otherwise! The game needs to find all results in order to sort them and throw most of them away.
  • The order of your selector arguments matters! Put cheaper ones first, and ones that filter out more things.
  • If type is not inverted and not a tag, it gets checked twice. Since the second check will always pass, it's best to put it at the end of the arguments.
  • Only distance, dx, dy, dz change how entities are found, the rest just filter. In other words, they are incurred costs that are harmful if checking them for each entity takes more time than the time spent processing entities they filtered out.
  • Cool fact: you can observe the arbitrariness of sort=arbitrary by running /say @e vs /say @e[distance=..9999999], the latter runs a totally different code path and iterates entities in a very different order!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment