1.16.5 Best way to keep track of mob kills?

Discussion in 'Spigot Plugin Development' started by gordanb740, Apr 14, 2021.

  1. I have made a plugin that drops loot whenever players kill mobs. However, in order to prevent abuse, I want the loot to stop dropping if the player has killed more than 10 of that specific mob in the last 12 hours. So if a player kills 10 zombies, they will then have to go kill different mobs in order to keep receiving loot. What would be the best way to do this?

    I thought about keeping it all inside a HashMap, but the tricky part is that the server has to keep track of the player, every mob the player has killed, and the amount of every mob the player has killed as well, so if a player kills 10 of each mob, that will be a lot of data to store for just that one player. And the trickiest part is that it should only look at the last 12 hours or so. I'm not quite sure how to do this, so I would appreciate any help!

    Saving it to a .yml file is not necessary. It's fine if server restarts reset the counter.
     
  2. You want to have a map with an object that keeps track of it for you.
    Map<UUID, KillTrackerObject> killTrackerMap = new HashMap();

    That object would keep track of what and when it died. That could look something like this:
    Code (Java):
    private Map<EntityType, Set<Long>> perMobTypeTracker = new HashMap<>();

    public boolean shouldConsiderKill(EntityType entityType) {
        Set<Long> killTimes = perMobTypeTracker.getOrDefault(entityType, new HashSet<>());
        killTimes.removeIf(timeAgoInMillis -> /*time is more than n amount of time ago*/);
        if (killTimes.size() >= /*max amount of kills the player should have*/)
            return false;

        killTimes.add(System.currentTimeMillis());
        perMobTypeTracker.put(entityType, killTimes);
        return true;
    }
    Or at least that's probably how I'd do it.
     
    • Winner Winner x 1
  3. Thank you very much for the help. I'm a bit overwhelmed though since I've never used a boolean like this before. Right now, it's giving me a few errors about the public boolean. Here is the code I have so far:

    Code (Java):
    private Map<EntityType, Set<Long>> perMobTypeTracker = new HashMap<>();
        String[] mobs = new String[] {
                "ZOMBIE",
                "SPIDER",
                "SKELETON"
        };
       
        @EventHandler
        public void onMobDeath(EntityDeathEvent e) {
            if(e.getEntity().getKiller() != null && e.getEntity().getKiller() instanceof Player) {
               
                for(String mob : mobs) {
                    EntityType entityType = EntityType.valueOf(mob);
                   
                   
                    public boolean shouldConsiderKill(EntityType boolEntityType) {
                        Set<Long> killTimes = perMobTypeTracker.getOrDefault(boolEntityType, new HashSet<>());
                        killTimes.removeIf(timeAgoInMillis -> 60000 /*time is more than n amount of time ago*/);
                        if (killTimes.size() >= 3 /*max amount of kills the player should have*/)
                            return false;

                        killTimes.add(System.currentTimeMillis());
                        perMobTypeTracker.put(entityType, killTimes);
                        return true;
                    }
                }
            }
        }
     
  4. You are declaring a method inside a method, of course that doesn't work. Also what do you mean with "never used a boolean like that before"? That's just a normal method that returns a boolean.
     
  5. Well, I'm not very familiar with that, I've only really used the most basic methods before, that's all. Anyway, I have moved the the method now, sorry about that, but the timeAgoInMillis line still returns error, claiming that it "cannot convert from int to boolean". Any idea why?
     
  6. Sorry, no offence, but you should really do some Java tutorials beforehand if you don't know what a method is or the difference between integer and boolean :p
     
  7. I've made plugins 10x more complex than this before and this is probably the last one I need to make so I'm not gonna start watching Java tutorials now when I'm so close. If I can't get this specific thing to work, I guess I will simply make it so that the limit is set on the total mobs killed instead of every specific type.
     
    • Funny Funny x 3
  8. I've used integers and booleans countless of times before and I know what they are. Like I said, I'm really close to being done with my last plugin, so if you're only here to laugh at me, then I don't see the point of this thread anymore. If you don't want to help, let's just end it here. I will figure it out on my own.
     
  9. I'll happily elaborate on the method I used that confused you. Lambdas look much scarier than they really are.
    killTimes.removeIf(timeAgoInMillis -> 60000 /*time is more than n amount of time ago*/);
    This is a fast and safe way to remove elements from a Collection if they match a certain criterion.

    In our case, we want to make sure that the kill happened more than 12 hours ago.
    12 hours in milliseconds is 12 (hours) * 60 (minutes) * 60 (seconds) * 1000 (milliseconds) = 43200000L.
    Simply means the time when it got killed needs to exceed the current time + that number.

    killTimes is the Collection, removeIf the method that accepts a predicate. Normally that predicate would look something like this:
    Code (Java):
    killTimes.removeIf(new Predicate<Long>() {
        @Override
        public boolean test(Long timeAgoInMillis) {
            return timeAgoInMillis + 43200000L <= System.currentTimeMillis();
        }
    });
    Thanks to lambda, we can simplify that a lot by turning the first part into a variable that gets provided (timeAgoInMillis) and an arrow to refer to the method that should return the value that it needs (a boolean).

    With lambda, our previous predicate now looks much shorter and more concise:
    killTimes.removeIf(timeAgoInMillis -> timeAgoInMillis + 43200000L <= System.currentTimeMillis());

    If everyone tells someone to learn somewhere else, the only thing they will learn is to not care.
     
    • Friendly Friendly x 1
  10. Thank you. This reply seriously made this horrible day a lot better. Also, the code you sent almost felt too good to be true, but it makes perfect sense now why it would work, and after some testing, it seems to work flawlessly! Again, thank you very much!

    This is what the working code now looks like in this specific class:
    Code (Java):
    String[] mobs = new String[] {
                "BLAZE",
                "CAVE_SPIDER",
                "CREEPER",
                "ENDERMAN",
                "GHAST",
                "HOGLIN",
                "HUSK",
                "MAGMA_CUBE",
                "PIGLIN",
                "PIGLIN_BRUTE",
                "SILVERFISH",
                "SKELETON",
                "SLIME",
                "SPIDER",
                "STRAY",
                "WITCH",
                "WITHER_SKELETON",
                "ZOGLIN",
                "ZOMBIE",
                "ZOMBIE_VILLAGER",
                "WITHER"
        };
     
        public boolean shouldConsiderKill(EntityType entityType) {
            Set<Long> killTimes = perMobTypeTracker.getOrDefault(entityType, new HashSet<>());
         
            killTimes.removeIf(new Predicate<Long>() {
                @Override
                public boolean test(Long timeAgoInMillis) {
                    return timeAgoInMillis + 43200000L <= System.currentTimeMillis();
                }
            });
         
            if (killTimes.size() >= 30) // Max amount of kills the player should have
                return false;

            killTimes.add(System.currentTimeMillis());
            perMobTypeTracker.put(entityType, killTimes);
            return true;
        }
     
        @EventHandler
        public void onMobDeath(EntityDeathEvent e) {
            if(e.getEntity().getKiller() != null && e.getEntity().getKiller() instanceof Player) {
             
                for(String mob : mobs) {
                    EntityType entityType = EntityType.valueOf(mob);
                 
                    if(e.getEntity().getType() == entityType) {
                        Random dice = new Random();
                        int fiftyFifty = dice.nextInt(2);
                     
                        if(fiftyFifty == 1 && shouldConsiderKill(entityType)) {
                            World world = e.getEntity().getWorld();
                            Location loc = e.getEntity().getLocation();
                         
                            ItemStack coin = new ItemStack(Material.GOLD_NUGGET);
                            ItemMeta coinMeta = coin.getItemMeta();
                            coinMeta.setDisplayName(ChatColor.GOLD + "941826");
                            coin.setItemMeta(coinMeta);
                         
                            world.dropItem(loc, coin);
                        }
                    }
                }
            }
        }
     
  11. Two things I feel like pointing out here:
    1. You probably want to use a Set of EntityTypes instead of converting a String array every single time.
      Set<EntityType> entityTypes = ImmutableSet.of(EntityType.ZOMBIE, EntityType.SPIDER /* etc. */);
      Which you then can quickly compare your killed entity with:
      if (!entityTypes.contains(e.getEntityType())) return;
    2. You also want the tracker to be per player.
      If functionality is all you care about, you can put that perMobTypeTracker map into another map with the UUID of the player as the key and it will work.
      If cleanliness is what matters to you, you can separate the tracking into its own manager class. You can get that manager class through your main plugin class, for example.
     
    • Like Like x 1
  12. Yes, the tracker also needs to look at every player separately. I tried to put the Map into another Map together with the UUID but I'm not sure how to actually make it only look at a certain UUID. This is the code I changed:
    Code (Java):
        private Map<UUID, Map<EntityType, Set<Long>>> perUUIDTracker = new HashMap<UUID, Map<EntityType, Set<Long>>>();
        private Map<EntityType, Set<Long>> perMobTypeTracker = new HashMap<EntityType, Set<Long>>();
    Code (Java):
        public boolean shouldConsiderKill(UUID uuid, EntityType entityType) {
            Set<Long> killTimes = perMobTypeTracker.getOrDefault(entityType, new HashSet<>());
           
            killTimes.removeIf(new Predicate<Long>() {
                @Override
                public boolean test(Long timeAgoInMillis) {
                    return timeAgoInMillis + 36000000L <= System.currentTimeMillis();
                }
            });
           
            if (killTimes.size() >= 30) // Max amount of kills the player should have
                return false;

            killTimes.add(System.currentTimeMillis());
            perMobTypeTracker.put(entityType, killTimes);
            perUUIDTracker.put(uuid, perMobTypeTracker);
            return true;
        }