1.17.1 Player Tracker - Trouble making it work with 2 players using a tracker

Discussion in 'Spigot Plugin Development' started by devJordan, Dec 11, 2021.

  1. Hey guys,

    As the title explains I've run into a bit of a wall with my plugin. So I've got a plugin that adds a player tracker and I knew I would run into this issue eventually, and now I have, where it runs into issues when 2 players try to use the tracker.

    A forewarning, I've put my entire code into the spoiler below because I'm not sure how much of the class is necessary to allow people to help because the issue involves a lot of aspects of the plugin.

    So the issues I see right now are that I have a BukkitRunnable which updates the direction of the compass if the target moves and that gets cancelled when the player's target is killed, but that would cancel the runnable for all players tracking.

    Another issue I suspect is that when one player clicks their tracker the target is going to be set the same for all assassins but I don't want that.

    So I had a setup earlier which used a hashmap to bind the key which was the assassin to a value of the target to try and make things a bit more independent and then I had some for each loops going on in the BukkitRunnable to search through the hashmap and see if players weren't there then trying to set their compass back to x=0,z=0 but it got complicated and messy and there were some issues with it so I went back to where it was cleaner and where it worked, so essentially square when in respect to the plugin working with multiple assassin's.

    TL;DR: I've got a tracking plugin, doesn't work with multiple trackers, please help :)

    Code (Java):
    package org.infamousmc.playertrackers;

    import net.md_5.bungee.api.ChatMessageType;
    import net.md_5.bungee.api.chat.TextComponent;
    import org.bukkit.Bukkit;
    import org.bukkit.ChatColor;
    import org.bukkit.Location;
    import org.bukkit.Material;
    import org.bukkit.command.Command;
    import org.bukkit.command.CommandSender;
    import org.bukkit.entity.Player;
    import org.bukkit.event.EventHandler;
    import org.bukkit.event.Listener;
    import org.bukkit.event.block.Action;
    import org.bukkit.event.entity.PlayerDeathEvent;
    import org.bukkit.event.player.PlayerInteractEvent;
    import org.bukkit.inventory.ItemFlag;
    import org.bukkit.inventory.ItemStack;
    import org.bukkit.inventory.meta.ItemMeta;
    import org.bukkit.plugin.java.JavaPlugin;
    import org.bukkit.scheduler.BukkitRunnable;

    import java.util.ArrayList;
    import java.util.List;
    import java.util.Random;

    public final class Main extends JavaPlugin implements Listener {

        Player selected;
        boolean trackerActive;

        public void onEnable() {
            // Plugin startup logic
            this.getServer().getPluginManager().registerEvents(this, this);

        public void onDisable() {
            // Plugin shutdown logic

        public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {

            // Give the player the tracker
            if (label.equalsIgnoreCase("tracker")) {
                if (!(sender instanceof Player)) return true;

                Player player = (Player) sender;
                return true;

            return false;

        // Activate Tracker
        public void onClick(PlayerInteractEvent event) {
            // Some checks to make sure the item is OUR compass
            if (!(event.getPlayer().getInventory().getItemInMainHand().getType().equals(Material.COMPASS))) return;
            if (!(event.getPlayer().getInventory().getItemInMainHand().getItemMeta().getDisplayName().contains("Assassin's Tracker"))) return;
            if (!(event.getPlayer().getInventory().getItemInMainHand().getItemMeta().hasLore())) return;
            if (!(event.getPlayer().getInventory().getItemInMainHand().getItemMeta().isUnbreakable())) return;

            // If OUR compass is right-clicked then we'll activate the tracker
            if (event.getAction() == Action.RIGHT_CLICK_AIR || event.getAction() == Action.RIGHT_CLICK_BLOCK) {
                // Put all online players (excluding those with the bypass permission) into a list
                ArrayList<Player> allPlayers = new ArrayList<Player>();
                for (Player players : Bukkit.getOnlinePlayers()) {
                    if (!(players.hasPermission("tracker.bypass"))) {
                // If our list equals 0 meaning there are no players online that don't bypass tracker send the player a message
                if (allPlayers.size() == 0) {
                    event.getPlayer().spigot().sendMessage(ChatMessageType.ACTION_BAR, new TextComponent(net.md_5.bungee.api.ChatColor.RED + "There are no trackable players online"));
                // Finally, get a random player from our list of trackable players
                int randomPlayer = new Random().nextInt(allPlayers.size());
                selected = allPlayers.get(randomPlayer);
                trackerActive = true;

                // Update the compass every 20 ticks (1 second) to point to the player's new location
                new BukkitRunnable() {
                    public void run() {
                        Location loc = selected.getLocation();

                        // If the tracker isn't active set the location to x=0,z=0
                        if (!trackerActive) {
                            event.getPlayer().spigot().sendMessage(ChatMessageType.ACTION_BAR, new TextComponent(net.md_5.bungee.api.ChatColor.RED + "Target killed, no longer tracking them."));
                            loc = new Location(event.getPlayer().getWorld(),0,0,0);
                }.runTaskTimer(this, 0L, 20L);

                // Send a message saying that we are officially tracking a player
                event.getPlayer().spigot().sendMessage(ChatMessageType.ACTION_BAR, new TextComponent(net.md_5.bungee.api.ChatColor.GREEN + "Tracking player"));


        // When the tracking player dies set the tracker to inactive
        public void onDeath(PlayerDeathEvent event) {
            if (event.getEntity() != selected) return;
            trackerActive = false;

        // Create our tracking compass item
        public ItemStack assassinTracker() {
            Material type;
            ItemStack item = new ItemStack(Material.COMPASS);
            ItemMeta meta = item.getItemMeta();

            meta.setDisplayName(ChatColor.DARK_RED + "Assassin's Tracker");
            List<String> lore = new ArrayList<String>();
            lore.add(ChatColor.GRAY + "Track a random player");
            lore.add(ChatColor.translateAlternateColorCodes('&', "&7(Right Click) &aSelect random player"));



            return item;

  2. Sorry your code was difficult to read on mobile. I would guess the main issue is storing a single instance of a player and active when multiple compasses can be active.

    Let's start from the beginning.

    - You want a tracker item that tracks the nearest player for each player that is holding the item.

    I think it would be most efficient to have a repeating task that updates all distances between players and then selects smallest one for each player.

    It might be easier to implement if you make a class to hold two UUIDs and the distance between these two players. That way you have a list to go through.
    Code (Text):
    public class PlayerDistance {
        private final UUID one;
        private final UUID two;
        double distance;

        // constructor, setter for distance, getters

        public boolean isForPlayer(UUID uuid) {
            return uuid.equals(one) || uuid.equals(two);
    Now you can loop through these and create a Map<UUID, PlayerDistance> that holds minimum value for each uuid you find.
    Code (Text):
    List<PlayerDistance> distances;
    Map<UUID, PlayerDistance> smallest

    for each distance
        double d =smallest.getOrDefault(distance.getOne(), distance.getDistance());
        if distance.getDistance() <= d
            smallest.put(distance.getOne(), distance);
        d =smallest.getOrDefault(distance.getTwo(), distance.getDistance());
        if distance.getDistance() <= d
            smallest.put(distance.getTwo(), distance);
    You can then use this map to set compass for each player.

    In order to update your distance list, loop through all players for all players so you get pairs of all players.

    Don't store Player objects since the player can leave the server and storing the player object then causes memory leak. UUIDs are better. (You can use Bukkit.getPlayer(uuid))

    You also need to clean the list and map of players who die (I'd track those separately in Set<UUID>) or leave
  3. Thank you for your reply! I appreciate the time and effort you put into explaining this! I should have been more clear however, the intent of this compass is quite specific for the server I plan to use it on so it may not be conventional, and the way it is set up now and is intended is for it to actually select a random player on the server rather than the nearest player.
  4. Ah then you can keep a Map<UUID, UUID> selectedPlayers instead :) (key: the player whose compass it is, value: the player who it is pointing to)
  5. So if I'm understanding this correctly all I would need is the map and then add the UUIDs instead of players (learned a new thing today, thank you!), is this correct?
    • Agree Agree x 1