Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: API for AFK and excluded players #88

Merged
merged 22 commits into from
Jul 10, 2021
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 58 additions & 5 deletions src/main/java/xyz/nkomarn/harbor/Harbor.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import xyz.nkomarn.harbor.api.AFKProvider;
import xyz.nkomarn.harbor.api.ExclusionProvider;
import xyz.nkomarn.harbor.api.LogicType;
import xyz.nkomarn.harbor.command.ForceSkipCommand;
import xyz.nkomarn.harbor.command.HarborCommand;
import xyz.nkomarn.harbor.listener.BedListener;
Expand All @@ -18,7 +21,6 @@
import java.util.Optional;

public class Harbor extends JavaPlugin {

private Config config;
private Checker checker;
private Messages messages;
Expand All @@ -43,16 +45,15 @@ public void onEnable() {
getCommand("harbor").setExecutor(new HarborCommand(this));
getCommand("forceskip").setExecutor(new ForceSkipCommand(this));

if (essentials == null) {
getLogger().info("Essentials not present- registering fallback AFK detection system.");
playerManager.registerFallbackListeners();
}


if (config.getBoolean("metrics")) {
new Metrics(this);
}
}



@Override
public void onDisable() {
for (World world : getServer().getWorlds()) {
Expand Down Expand Up @@ -85,6 +86,58 @@ public PlayerManager getPlayerManager() {
return playerManager;
}

/**
* Add an {@link ExclusionProvider} to harbor, so an external plugin can set a player to be excluded from the sleep count
*
* @param provider An external implementation of an {@link ExclusionProvider}, provided by an implementing plugin
*
* @see ExclusionProvider
* @see Checker#addExclusionProvider(ExclusionProvider)
*/
@SuppressWarnings("unused")
public void addExclusionProvider(ExclusionProvider provider) {
checker.addExclusionProvider(provider);
}

/**
* Remove an {@link ExclusionProvider}, for use by an external plugin
*
* @param provider The provider to remove
*
* @see #addExclusionProvider(ExclusionProvider)
*/
@SuppressWarnings("unused")
public void removeExclusionProvider(ExclusionProvider provider){
checker.removeExclusionProvider(provider);
}

/**
* Add an {@link AFKProvider} to harbor, so an external plugin can provide an AFK status to harbor
*
* @param provider An external implementation of an {@link AFKProvider}, provided by an implementing plugin
*
* @see AFKProvider
* @see PlayerManager#addAfkProvider(AFKProvider, LogicType)
*/
@SuppressWarnings("unused")
public void addAFKProvider(AFKProvider provider, LogicType type) {
playerManager.addAfkProvider(provider, type);
}

/**
* Removes an {@link ExclusionProvider}
* @param provider The provider to remove
*
* @see #addAFKProvider(AFKProvider, LogicType)
*/
@SuppressWarnings("unused")
public void removeAFKProvider(AFKProvider provider){
playerManager.removeAfkProvider(provider);
}

/**
* @return The current instance of Essentials ({@link Essentials}, wrapped in {@link Optional}
*/
@NotNull
public Optional<Essentials> getEssentials() {
return Optional.ofNullable(essentials);
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/xyz/nkomarn/harbor/api/AFKProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package xyz.nkomarn.harbor.api;

import org.bukkit.entity.Player;

/**
* The {@link AFKProvider} interface provides a way for an implementing
* class in an external plugin to provide a way for external plugins to tell
* Harbor if a Player is AFK, in case of a custom AFK implementation
*
* @see xyz.nkomarn.harbor.Harbor#addExclusionProvider(ExclusionProvider)
*/
public interface AFKProvider {
/**
* Tests if a {@link Player} is AFK
*
* @param player The {@link Player} that is being checked
*
* @return If the player is afk (true) or not (false)
*/
boolean isAFK(Player player);
}
21 changes: 21 additions & 0 deletions src/main/java/xyz/nkomarn/harbor/api/ExclusionProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package xyz.nkomarn.harbor.api;

import org.bukkit.entity.Player;

/**
* The {@link ExclusionProvider} interface provides a way for an implementing
* class in an external plugin to provide a way for external plugins to control
* programmatically if a player should be excluded from the cap
*
* @see xyz.nkomarn.harbor.Harbor#addExclusionProvider(ExclusionProvider)
*/
public interface ExclusionProvider {
/**
* Tests if a {@link Player} is excluded from the sleep checks for Harbor
*
* @param player The {@link Player} that is being checked
*
* @return If the player is excluded (true) or not (false)
*/
boolean isExcluded(Player player);
}
18 changes: 18 additions & 0 deletions src/main/java/xyz/nkomarn/harbor/api/LogicType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package xyz.nkomarn.harbor.api;

import org.bukkit.configuration.Configuration;
import org.jetbrains.annotations.NotNull;

/**
* An enum to represent the type of logic to be used when combining multiple Providers
*/
public enum LogicType {
AND,

@SuppressWarnings("unused")
OR;

public static LogicType fromConfig(@NotNull Configuration configuration, String path, LogicType defaultType) {
return valueOf(configuration.getString(path, defaultType.toString()).toUpperCase().trim());
}
}
150 changes: 150 additions & 0 deletions src/main/java/xyz/nkomarn/harbor/listener/AfkListener.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package xyz.nkomarn.harbor.listener;

import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitRunnable;
import xyz.nkomarn.harbor.Harbor;
import xyz.nkomarn.harbor.provider.DefaultAFKProvider;

import java.util.ArrayDeque;
import java.util.Queue;
import java.util.function.Function;
import java.util.stream.Collectors;

public final class AfkListener implements Listener {
private final DefaultAFKProvider afkProvider;
private Queue<AfkPlayer> players;
private PlayerMovementChecker movementChecker;

public AfkListener(DefaultAFKProvider afkProvider) {
this.afkProvider = afkProvider;
JavaPlugin.getPlugin(Harbor.class).getLogger().info("Initializing fallback AFK detection system. Fallback AFK system is not enabled at this time");
}

/**
* Provides a way to start the listener
*/
public void start() {
JavaPlugin plugin = JavaPlugin.getPlugin(Harbor.class);

Choose a reason for hiding this comment

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

Please pass the Harbor instance trough the constructor, to maintain consistency with the rest of the code.

players = new ArrayDeque<>();
movementChecker = new PlayerMovementChecker();

// Populate the queue with any existing players
players.addAll(Bukkit.getOnlinePlayers().stream().map((Function<Player, AfkPlayer>) AfkPlayer::new).collect(Collectors.toSet()));

// Register listeners after populating the queue
Bukkit.getServer().getPluginManager().registerEvents(this, plugin);

// We want every player to get a check every 20 ticks. The runnable smooths out checking a certain
// percentage of players over all 20 ticks. Thusly, the runnable must run on every tick
movementChecker.runTaskTimer(plugin, 0, 1);

JavaPlugin.getPlugin(Harbor.class).getLogger().info("Fallback AFK detection system is enabled");

Choose a reason for hiding this comment

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

Pass Harbor instance through the constructor

(ps you already had the harbor instance assigned to a variable in at the top of the method 🙃 )

}

/**
* Provides a way to halt the listener
*/
public void stop() {
movementChecker.cancel();
HandlerList.unregisterAll(this);
players = null;
JavaPlugin.getPlugin(Harbor.class).getLogger().info("Fallback AFK detection system is disabled");

Choose a reason for hiding this comment

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

Pass Harbor instance through the constructor

}

@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onChat(AsyncPlayerChatEvent event) {
afkProvider.updateActivity(event.getPlayer());
}

@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onCommand(PlayerCommandPreprocessEvent event) {
afkProvider.updateActivity(event.getPlayer());
}

@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onInventoryClick(InventoryClickEvent event) {
afkProvider.updateActivity((Player) event.getWhoClicked());
}

@EventHandler(priority = EventPriority.MONITOR)
public void onJoin(PlayerJoinEvent event) {
players.add(new AfkPlayer(event.getPlayer()));
afkProvider.updateActivity(event.getPlayer());
}

@EventHandler(priority = EventPriority.MONITOR)
public void onLeave(PlayerQuitEvent event) {
players.remove(new AfkPlayer(event.getPlayer()));
afkProvider.removePlayer(event.getPlayer().getUniqueId());
}

/**
* Internal class for handling the task of checking player movement; Is a separate task so that we can cancel and restart it easily
*/
private final class PlayerMovementChecker extends BukkitRunnable {
private double checksToMake = 0;
@Override
public void run() {
if(players.isEmpty()){
checksToMake = 0;
return;
}

// We want every player to get a check every 20 ticks. Therefore we check 1/20th of the players
for (checksToMake += players.size() / 20.0D; checksToMake > 0 && !players.isEmpty(); checksToMake--) {
AfkPlayer afkPlayer = players.poll();
if (afkPlayer.changed()) {
afkProvider.updateActivity(afkPlayer.player);
}
players.add(afkPlayer);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

just use a double :D This wont kill anyone and will make the logic simpler :D
Currently you will check a player in an unstable state. If one player is on the server the player will be checked every tick.
4 Players -> Every 5 Ticks
10 Players -> Every 10 Ticks
Only after 20 players online the splitting will have the effect I want.
That I used a double had the thought that I might want to check only a friction of the players and I will wait until I have a full player 1 > checksToMake. this will allow empty runs when less then 20 players are online and when more then 20 players are online it will check in some runs more than one,
Doubles arent bad for loops :D

Otherwise we could just merge it and I will apply the optimizations by myself in a new PR. Your choice.

Or still the choice of the maintainers of this project.

        if (players.isEmpty()) return;

        checksToMake += players.size() / 20.0;

        while (checksToMake > 1) {
            AfkPlayer afkPlayer = players.poll();
            if (afkPlayer.changed()) {
                playerManager.updateActivity(afkPlayer.player);
            }
            players.add(afkPlayer);
            checksToMake--;
        }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Converted to double, using for loop syntax:

 private final class PlayerMovementChecker extends BukkitRunnable {
        private double checksToMake = 0;
        @Override
        public void run() {
            if(players.isEmpty()){
                checksToMake = 0;
                return;
            }

            // We want every player to get a check every 20 ticks. Therefore we check 1/20th of the players
            for (checksToMake += players.size() / 20.0D; checksToMake > 0 && !players.isEmpty(); checksToMake--) {
                AfkPlayer afkPlayer = players.poll();
                if (afkPlayer.changed()) {
                    afkProvider.updateActivity(afkPlayer.player);
                }
                players.add(afkPlayer);
            }
        }
    }

}
}



private static final class AfkPlayer {
private final Player player;
private int hash;

public AfkPlayer(Player player) {
this.player = player;
}

/**
* Check if the player changed its position since the last check
*
* @return true if the position changed
*/
boolean changed() {
int hash = player.getLocation().hashCode();
boolean changed = hash != this.hash;
this.hash = hash;
return changed;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AfkPlayer afkPlayer = (AfkPlayer) o;
return player.getUniqueId().equals(afkPlayer.player.getUniqueId());
}

@Override
public int hashCode() {
return player.getUniqueId().hashCode();
}
}
}
12 changes: 7 additions & 5 deletions src/main/java/xyz/nkomarn/harbor/listener/BedListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
import org.bukkit.event.player.PlayerBedLeaveEvent;
import org.jetbrains.annotations.NotNull;
import xyz.nkomarn.harbor.Harbor;
import xyz.nkomarn.harbor.task.Checker;
import xyz.nkomarn.harbor.util.Messages;
import xyz.nkomarn.harbor.util.PlayerManager;

import java.util.concurrent.TimeUnit;
import java.time.Instant;
import java.time.temporal.ChronoUnit;

public class BedListener implements Listener {

Expand All @@ -38,7 +40,7 @@ public void onBedEnter(PlayerBedEnterEvent event) {
}

Bukkit.getScheduler().runTaskLater(harbor, () -> {
playerManager.setCooldown(player, System.currentTimeMillis());
playerManager.setCooldown(player, Instant.now());
harbor.getMessages().sendWorldChatMessage(event.getBed().getWorld(), messages.prepareMessage(
player, harbor.getConfiguration().getString("messages.chat.player-sleeping"))
);
Expand All @@ -52,7 +54,7 @@ public void onBedLeave(PlayerBedLeaveEvent event) {
}

Bukkit.getScheduler().runTaskLater(harbor, () -> {
playerManager.setCooldown(event.getPlayer(), System.currentTimeMillis());
playerManager.setCooldown(event.getPlayer(), Instant.now());
harbor.getMessages().sendWorldChatMessage(event.getBed().getWorld(), messages.prepareMessage(
event.getPlayer(), harbor.getConfiguration().getString("messages.chat.player-left-bed"))
);
Expand All @@ -70,11 +72,11 @@ private boolean isMessageSilenced(@NotNull Player player) {
return true;
}

if (harbor.getChecker().isVanished(player)) {
if (Checker.isVanished(player)) {
return true;
}

int cooldown = harbor.getConfiguration().getInteger("messages.chat.message-cooldown");
return TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis() - playerManager.getCooldown(player)) < cooldown;
return playerManager.getCooldown(player).until(Instant.now(), ChronoUnit.MINUTES) < cooldown;
}
}
Loading