diff --git a/src/main/java/xyz/nkomarn/harbor/Harbor.java b/src/main/java/xyz/nkomarn/harbor/Harbor.java index 64a1ede..0f85ce4 100644 --- a/src/main/java/xyz/nkomarn/harbor/Harbor.java +++ b/src/main/java/xyz/nkomarn/harbor/Harbor.java @@ -5,6 +5,9 @@ import org.bukkit.World; 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; @@ -18,7 +21,6 @@ import java.util.Arrays; import java.util.Optional; public class Harbor extends JavaPlugin { - private Config config; private Checker checker; private Messages messages; @@ -43,16 +45,15 @@ public class Harbor extends JavaPlugin { 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()) { @@ -85,6 +86,58 @@ public class Harbor extends JavaPlugin { 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 getEssentials() { return Optional.ofNullable(essentials); diff --git a/src/main/java/xyz/nkomarn/harbor/api/AFKProvider.java b/src/main/java/xyz/nkomarn/harbor/api/AFKProvider.java new file mode 100644 index 0000000..ad0dde0 --- /dev/null +++ b/src/main/java/xyz/nkomarn/harbor/api/AFKProvider.java @@ -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); +} diff --git a/src/main/java/xyz/nkomarn/harbor/api/ExclusionProvider.java b/src/main/java/xyz/nkomarn/harbor/api/ExclusionProvider.java new file mode 100644 index 0000000..8ff3a95 --- /dev/null +++ b/src/main/java/xyz/nkomarn/harbor/api/ExclusionProvider.java @@ -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); +} diff --git a/src/main/java/xyz/nkomarn/harbor/api/LogicType.java b/src/main/java/xyz/nkomarn/harbor/api/LogicType.java new file mode 100644 index 0000000..3f00b46 --- /dev/null +++ b/src/main/java/xyz/nkomarn/harbor/api/LogicType.java @@ -0,0 +1,15 @@ +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, OR; + + public static LogicType fromConfig(@NotNull Configuration configuration, String path, LogicType defaultType) { + return valueOf(configuration.getString(path, defaultType.toString()).toUpperCase().trim()); + } +} diff --git a/src/main/java/xyz/nkomarn/harbor/listener/AfkListener.java b/src/main/java/xyz/nkomarn/harbor/listener/AfkListener.java new file mode 100644 index 0000000..b0d1c76 --- /dev/null +++ b/src/main/java/xyz/nkomarn/harbor/listener/AfkListener.java @@ -0,0 +1,163 @@ +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.scheduler.BukkitRunnable; +import org.jetbrains.annotations.NotNull; +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 players; + private PlayerMovementChecker movementChecker; + private final Harbor harbor; + private boolean status; + + public AfkListener(@NotNull DefaultAFKProvider afkProvider) { + this.afkProvider = afkProvider; + this.harbor = afkProvider.getHarbor(); + harbor.getLogger().info("Initializing fallback AFK detection system. Fallback AFK system is not enabled at this time"); + status = false; + } + + /** + * Provides a way to start the listener + */ + public void start() { + if(!status) { + status = true; + players = new ArrayDeque<>(); + movementChecker = new PlayerMovementChecker(); + + // Populate the queue with any existing players + players.addAll(Bukkit.getOnlinePlayers().stream().map((Function) AfkPlayer::new).collect(Collectors.toSet())); + + // Register listeners after populating the queue + Bukkit.getServer().getPluginManager().registerEvents(this, harbor); + + // 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(harbor, 0, 1); + + harbor.getLogger().info("Fallback AFK detection system is enabled"); + } else { + harbor.getLogger().info("Fallback AFK detection system was already enabled"); + } + } + + /** + * Provides a way to halt the listener + */ + public void stop() { + if(status) { + status = false; + movementChecker.cancel(); + HandlerList.unregisterAll(this); + players = null; + harbor.getLogger().info("Fallback AFK detection system is disabled"); + } else { + harbor.getLogger().info("Fallback AFK detection system was already disabled"); + } + } + + @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() / 20D; 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 locationHash; + + public AfkPlayer(Player player) { + this.player = player; + locationHash = player.getEyeLocation().hashCode(); + } + + /** + * Check if the player changed its position since the last check + * + * @return true if the position changed + */ + boolean changed() { + int previousLocation = locationHash; + locationHash = player.getEyeLocation().hashCode(); + return previousLocation != locationHash; + } + + @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(); + } + } +} diff --git a/src/main/java/xyz/nkomarn/harbor/listener/BedListener.java b/src/main/java/xyz/nkomarn/harbor/listener/BedListener.java index 3a32ff3..af787f4 100644 --- a/src/main/java/xyz/nkomarn/harbor/listener/BedListener.java +++ b/src/main/java/xyz/nkomarn/harbor/listener/BedListener.java @@ -9,10 +9,12 @@ import org.bukkit.event.player.PlayerBedEnterEvent; 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 { @@ -38,7 +40,7 @@ public class BedListener implements Listener { } 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")) ); @@ -52,7 +54,7 @@ public class BedListener implements Listener { } 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")) ); @@ -70,11 +72,11 @@ public class BedListener implements Listener { 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; } } diff --git a/src/main/java/xyz/nkomarn/harbor/provider/DefaultAFKProvider.java b/src/main/java/xyz/nkomarn/harbor/provider/DefaultAFKProvider.java new file mode 100644 index 0000000..7a4e624 --- /dev/null +++ b/src/main/java/xyz/nkomarn/harbor/provider/DefaultAFKProvider.java @@ -0,0 +1,91 @@ +package xyz.nkomarn.harbor.provider; + +import org.bukkit.entity.Player; +import org.bukkit.event.Listener; +import org.jetbrains.annotations.NotNull; +import xyz.nkomarn.harbor.Harbor; +import xyz.nkomarn.harbor.api.AFKProvider; +import xyz.nkomarn.harbor.listener.AfkListener; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.logging.Level; + +/** + * The default AFK provider, which should be disabled if any others are registered + */ +public final class DefaultAFKProvider implements AFKProvider, Listener { + private final boolean enabled; + private Map playerActivity; + private final AfkListener listener; + private final int timeout; + private final Harbor harbor; + + public DefaultAFKProvider(@NotNull Harbor harbor) { + this.harbor = harbor; + if (enabled = (harbor.getConfig().getBoolean("afk-detection.fallback-enabled", true))) { + timeout = harbor.getConfig().getInt("afk-detection.fallback-timeout", 15); + listener = new AfkListener(this); + enableListeners(); + } else { + harbor.getLogger().info("Not registering fallback AFK detection system."); + listener = null; + timeout = -1; + } + } + + @Override + public boolean isAFK(Player player) { + if (!enabled || !playerActivity.containsKey(player.getUniqueId())) { + return false; + } + + long minutes = playerActivity.get(player.getUniqueId()).until(Instant.now(), ChronoUnit.MINUTES); + return minutes >= timeout; + } + + /** + * Sets the given player's last activity to the current timestamp. + * + * @param player The player to update. + */ + public void updateActivity(@NotNull Player player) { + playerActivity.put(player.getUniqueId(), Instant.now()); + } + + + /** + * Enables Harbor's fallback listeners for AFK detection if other AFKProviders are not present. + */ + public void enableListeners() { + if (enabled) { + harbor.getLogger().log(Level.FINE, "Enabling listeners for Default AFK Provider"); + playerActivity = new HashMap<>(); + listener.start(); + } + } + + /** + * Disables Harbor's fallback listeners for AFK detection if other AFKProviders are present. + */ + public void disableListeners() { + if (enabled) { + harbor.getLogger().log(Level.FINE, "Disabling listeners for Default AFK Provider"); + listener.stop(); + playerActivity = null; + } + } + + + public void removePlayer(UUID uniqueId) { + playerActivity.remove(uniqueId); + } + + @NotNull + public Harbor getHarbor() { + return harbor; + } +} diff --git a/src/main/java/xyz/nkomarn/harbor/provider/EssentialsAFKProvider.java b/src/main/java/xyz/nkomarn/harbor/provider/EssentialsAFKProvider.java new file mode 100644 index 0000000..e12a254 --- /dev/null +++ b/src/main/java/xyz/nkomarn/harbor/provider/EssentialsAFKProvider.java @@ -0,0 +1,32 @@ +package xyz.nkomarn.harbor.provider; + +import com.earth2me.essentials.Essentials; +import com.earth2me.essentials.User; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import xyz.nkomarn.harbor.Harbor; +import xyz.nkomarn.harbor.api.AFKProvider; + +/** + * An {@link AFKProvider} that uses Essentials; can be used as an example of how external + * plugins can implement an {@link AFKProvider} + */ +public final class EssentialsAFKProvider implements AFKProvider { + private final Harbor harbor; + private final Essentials essentials; + + public EssentialsAFKProvider(@NotNull Harbor harbor, @NotNull Essentials essentials) { + this.harbor = harbor; + this.essentials = essentials; + } + + @Override + public boolean isAFK(Player player) { + if(harbor.getConfig().getBoolean("afk-detection.essentials-enabled", true)) { + User user = essentials.getUser(player); + return user != null && user.isAfk(); + } else { + return false; + } + } +} diff --git a/src/main/java/xyz/nkomarn/harbor/provider/GameModeExclusionProvider.java b/src/main/java/xyz/nkomarn/harbor/provider/GameModeExclusionProvider.java new file mode 100644 index 0000000..0009c8b --- /dev/null +++ b/src/main/java/xyz/nkomarn/harbor/provider/GameModeExclusionProvider.java @@ -0,0 +1,22 @@ +package xyz.nkomarn.harbor.provider; + +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import xyz.nkomarn.harbor.Harbor; +import xyz.nkomarn.harbor.api.ExclusionProvider; + +/** + * A class-base {@link ExclusionProvider} which handles exclusions based on {@link org.bukkit.GameMode} + */ +public final class GameModeExclusionProvider implements ExclusionProvider { + private final Harbor harbor; + + public GameModeExclusionProvider(@NotNull Harbor harbor) { + this.harbor = harbor; + } + + @Override + public boolean isExcluded(Player player) { + return harbor.getConfig().getBoolean("exclusions.exclude-" + player.getGameMode().toString().toLowerCase(), false); + } +} diff --git a/src/main/java/xyz/nkomarn/harbor/task/Checker.java b/src/main/java/xyz/nkomarn/harbor/task/Checker.java index db57070..9ae234d 100644 --- a/src/main/java/xyz/nkomarn/harbor/task/Checker.java +++ b/src/main/java/xyz/nkomarn/harbor/task/Checker.java @@ -1,17 +1,16 @@ package xyz.nkomarn.harbor.task; import org.bukkit.Bukkit; -import org.bukkit.GameMode; import org.bukkit.World; -import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; - import org.bukkit.entity.Pose; import org.bukkit.metadata.MetadataValue; import org.bukkit.scheduler.BukkitRunnable; import org.jetbrains.annotations.NotNull; import xyz.nkomarn.harbor.Harbor; +import xyz.nkomarn.harbor.api.ExclusionProvider; +import xyz.nkomarn.harbor.provider.GameModeExclusionProvider; import xyz.nkomarn.harbor.util.Config; import xyz.nkomarn.harbor.util.Messages; @@ -23,15 +22,28 @@ import java.util.UUID; import static java.util.stream.Collectors.toList; public class Checker extends BukkitRunnable { - + private final Set providers; private final Harbor harbor; private final Set skippingWorlds; public Checker(@NotNull Harbor harbor) { this.harbor = harbor; this.skippingWorlds = new HashSet<>(); + this.providers = new HashSet<>(); - runTaskTimerAsynchronously(harbor, 0L, harbor.getConfiguration().getInteger("interval") * 20); + // GameModeExclusionProvider checks each case on its own + providers.add(new GameModeExclusionProvider(harbor)); + + // The others are simple enough that we can use lambdas + providers.add(player -> harbor.getConfig().getBoolean("exclusions.ignored-permission", true) && player.hasPermission("harbor.ignored")); + providers.add(player -> harbor.getConfig().getBoolean("exclusions.exclude-vanished", false) && isVanished(player)); + providers.add(player -> harbor.getConfig().getBoolean("exclusions.exclude-afk", false) && harbor.getPlayerManager().isAfk(player)); + + int interval = harbor.getConfiguration().getInteger("interval"); + // Default to 1 if its invalid + if (interval <= 0) + interval = 1; + runTaskTimerAsynchronously(harbor, 0L, interval * 20L); } @Override @@ -45,6 +57,7 @@ public class Checker extends BukkitRunnable { * Checks if a given world is applicable for night skipping. * * @param world The world to check. + * * @return Whether Harbor should run the night skipping check below. */ private boolean validateWorld(@NotNull World world) { @@ -103,6 +116,7 @@ public class Checker extends BukkitRunnable { * Checks if the time in a given world is considered to be night. * * @param world The world to check. + * * @return Whether it is currently night in the provided world. */ private boolean isNight(@NotNull World world) { @@ -113,6 +127,7 @@ public class Checker extends BukkitRunnable { * Checks if a current world has been blacklisted (or whitelisted) in the configuration. * * @param world The world to check. + * * @return Whether a world is excluded from Harbor checks. */ public boolean isBlacklisted(@NotNull World world) { @@ -129,22 +144,18 @@ public class Checker extends BukkitRunnable { * Checks if a given player is in a vanished state. * * @param player The player to check. + * * @return Whether the provided player is vanished. */ - public boolean isVanished(@NotNull Player player) { - for (MetadataValue meta : player.getMetadata("vanished")) { - if (meta.asBoolean()) { - return true; - } - } - - return false; + public static boolean isVanished(@NotNull Player player) { + return player.getMetadata("vanished").stream().anyMatch(MetadataValue::asBoolean); } /** * Returns the amount of players that should be counted for Harbor's checks, ignoring excluded players. * * @param world The world for which to check player count. + * * @return The amount of players in a given world, minus excluded players. */ public int getPlayers(@NotNull World world) { @@ -155,6 +166,7 @@ public class Checker extends BukkitRunnable { * Returns a list of all sleeping players in a given world. * * @param world The world in which to check for sleeping players. + * * @return A list of all currently sleeping players in the provided world. */ @NotNull @@ -168,6 +180,7 @@ public class Checker extends BukkitRunnable { * Returns the amount of players that must be sleeping to skip the night in the given world. * * @param world The world for which to check skip amount. + * * @return The amount of players that need to sleep to skip the night. */ public int getSkipAmount(@NotNull World world) { @@ -178,6 +191,7 @@ public class Checker extends BukkitRunnable { * Returns the amount of players that are still needed to skip the night in a given world. * * @param world The world for which to check the amount of needed players. + * * @return The amount of players that still need to get into bed to start the night skipping task. */ public int getNeeded(@NotNull World world) { @@ -189,6 +203,7 @@ public class Checker extends BukkitRunnable { * Returns a list of players that are considered to be excluded from Harbor's player count checks. * * @param world The world for which to check for excluded players. + * * @return A list of excluded players in the given world. */ @NotNull @@ -202,34 +217,18 @@ public class Checker extends BukkitRunnable { * Checks if a given player is considered excluded from Harbor's checks. * * @param player The player to check. + * * @return Whether the given player is excluded. */ private boolean isExcluded(@NotNull Player player) { - ConfigurationSection exclusions = harbor.getConfig().getConfigurationSection("exclusions"); - - if (exclusions == null) { - return false; - } - - boolean excludedByAdventure = exclusions.getBoolean("exclude-adventure", false) && player.getGameMode() == GameMode.ADVENTURE; - boolean excludedByCreative = exclusions.getBoolean("exclude-creative", false) && player.getGameMode() == GameMode.CREATIVE; - boolean excludedBySpectator = exclusions.getBoolean("exclude-spectator", false) && player.getGameMode() == GameMode.SPECTATOR; - boolean excludedByPermission = exclusions.getBoolean("ignored-permission", false) && player.hasPermission("harbor.ignored"); - boolean excludedByVanish = exclusions.getBoolean("exclude-vanished", false) && isVanished(player); - boolean excludedByAfk = exclusions.getBoolean("exclude-afk", false) && harbor.getPlayerManager().isAfk(player); - - return excludedByAdventure - || excludedByCreative - || excludedBySpectator - || excludedByPermission - || excludedByVanish - || excludedByAfk; + return providers.stream().anyMatch(provider -> provider.isExcluded(player)); } /** * Checks whether the night is currently being skipped in the given world. * * @param world The world to check. + * * @return Whether the night is currently skipping in the provided world. */ public boolean isSkipping(@NotNull World world) { @@ -302,4 +301,19 @@ public class Checker extends BukkitRunnable { runnable.run(); } } + + /** + * Adds an {@link ExclusionProvider}, which will be checked as a condition. All Exclusions will be ORed together + * on which to exclude a given player + */ + public void addExclusionProvider(ExclusionProvider provider) { + providers.add(provider); + } + + /** + * Removes an {@link ExclusionProvider} + */ + public void removeExclusionProvider(ExclusionProvider provider) { + providers.remove(provider); + } } diff --git a/src/main/java/xyz/nkomarn/harbor/util/Config.java b/src/main/java/xyz/nkomarn/harbor/util/Config.java index 17789b4..7aaec1f 100644 --- a/src/main/java/xyz/nkomarn/harbor/util/Config.java +++ b/src/main/java/xyz/nkomarn/harbor/util/Config.java @@ -8,7 +8,6 @@ import xyz.nkomarn.harbor.Harbor; import java.util.List; public class Config { - private final Harbor harbor; public Config(@NotNull Harbor harbor) { diff --git a/src/main/java/xyz/nkomarn/harbor/util/PlayerManager.java b/src/main/java/xyz/nkomarn/harbor/util/PlayerManager.java index 99aa76e..63527b7 100644 --- a/src/main/java/xyz/nkomarn/harbor/util/PlayerManager.java +++ b/src/main/java/xyz/nkomarn/harbor/util/PlayerManager.java @@ -1,44 +1,53 @@ package xyz.nkomarn.harbor.util; -import com.earth2me.essentials.Essentials; -import com.earth2me.essentials.User; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; 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.PlayerMoveEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.jetbrains.annotations.NotNull; import xyz.nkomarn.harbor.Harbor; +import xyz.nkomarn.harbor.api.AFKProvider; +import xyz.nkomarn.harbor.api.LogicType; +import xyz.nkomarn.harbor.provider.DefaultAFKProvider; +import xyz.nkomarn.harbor.provider.EssentialsAFKProvider; +import java.time.Instant; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; -import java.util.Optional; +import java.util.Set; import java.util.UUID; -import java.util.concurrent.TimeUnit; public class PlayerManager implements Listener { - - private final Harbor harbor; - private final Map cooldowns; - private final Map playerActivity; + private final Map cooldowns; + private final Set andedProviders; + private final Set oredProviders; + private final DefaultAFKProvider defaultProvider; public PlayerManager(@NotNull Harbor harbor) { - this.harbor = harbor; this.cooldowns = new HashMap<>(); - this.playerActivity = new HashMap<>(); + this.andedProviders = new HashSet<>(); + this.oredProviders = new HashSet<>(); + this.defaultProvider = new DefaultAFKProvider(harbor); + + updateListeners(); + if (harbor.getEssentials().isPresent()) { + addAfkProvider(new EssentialsAFKProvider(harbor, harbor.getEssentials().get()), + LogicType.fromConfig(harbor.getConfig(), "essentials-detection-mode", LogicType.AND)); + } else { + harbor.getLogger().info("Essentials not present - not registering Essentials integration"); + } } /** * Gets the last tracked cooldown time for a given player. * * @param player The player for which to return cooldown time. + * * @return The player's last cooldown time. */ - public long getCooldown(@NotNull Player player) { - return cooldowns.getOrDefault(player.getUniqueId(), 0L); + public Instant getCooldown(@NotNull Player player) { + return cooldowns.getOrDefault(player.getUniqueId(), Instant.MIN); } /** @@ -47,7 +56,7 @@ public class PlayerManager implements Listener { * @param player The player for which to set cooldown. * @param cooldown The cooldown value. */ - public void setCooldown(@NotNull Player player, long cooldown) { + public void setCooldown(@NotNull Player player, Instant cooldown) { cooldowns.put(player.getUniqueId(), cooldown); } @@ -62,73 +71,60 @@ public class PlayerManager implements Listener { * Checks if a player is considered "AFK" for Harbor's player checks. * * @param player The player to check. + * * @return Whether the player is considered AFK. */ public boolean isAfk(@NotNull Player player) { - if (!harbor.getConfiguration().getBoolean("afk-detection.enabled")) { - return false; + // If there are no providers registered, we go to the default provider + if(oredProviders.isEmpty() && andedProviders.isEmpty()){ + return defaultProvider.isAFK(player); } - - Optional essentials = harbor.getEssentials(); - if (essentials.isPresent()) { - User user = essentials.get().getUser(player); - - if (user != null) { - return user.isAfk(); - } - } - - if (!playerActivity.containsKey(player.getUniqueId())) { - return false; - } - - long minutes = TimeUnit.MILLISECONDS.toMinutes(System.currentTimeMillis() - playerActivity.get(player.getUniqueId())); - return minutes >= harbor.getConfiguration().getInteger("afk-detection.timeout"); - } - - /** - * Sets the given player's last activity to the current timestamp. - * - * @param player The player to update. - */ - public void updateActivity(@NotNull Player player) { - playerActivity.put(player.getUniqueId(), System.currentTimeMillis()); - } - - /** - * Registers Harbor's fallback listeners for AFK detection if Essentials is not present. - */ - public void registerFallbackListeners() { - harbor.getServer().getPluginManager().registerEvents(new AfkListeners(), harbor); + return oredProviders.stream().anyMatch(provider -> provider.isAFK(player)) || + (!andedProviders.isEmpty() && andedProviders.stream().allMatch(provider -> provider.isAFK(player))); } @EventHandler - public void onQuit(PlayerQuitEvent event) { + public void onQuit(@NotNull PlayerQuitEvent event) { UUID uuid = event.getPlayer().getUniqueId(); cooldowns.remove(uuid); - playerActivity.remove(uuid); } - private final class AfkListeners implements Listener { - @EventHandler(ignoreCancelled = true) - public void onChat(AsyncPlayerChatEvent event) { - updateActivity(event.getPlayer()); + /** + * Add an AFK Provider to harbor, so an external plugin can provide an AFK status to harbor + * + * @param provider The {@link AFKProvider} to be added + * @param logicType The type of logic (And or Or, {@link LogicType}) to be used with the given provider + */ + public void addAfkProvider(@NotNull AFKProvider provider, @NotNull LogicType logicType) { + switch (logicType){ + case AND: + andedProviders.add(provider); + break; + case OR: + oredProviders.add(provider); + break; + default: + throw new IllegalStateException("Invalid logic type specified"); } + updateListeners(); + } - @EventHandler(ignoreCancelled = true) - public void onCommand(PlayerCommandPreprocessEvent event) { - updateActivity(event.getPlayer()); - } + /** + * Remove an AFK provider from Harbor, provided for external plugins. + * @param provider the {@link AFKProvider} to be removed. + */ + public void removeAfkProvider(@NotNull AFKProvider provider) { + andedProviders.remove(provider); + oredProviders.remove(provider); + updateListeners(); + } - @EventHandler(ignoreCancelled = true) - public void onMove(PlayerMoveEvent event) { - updateActivity(event.getPlayer()); - } - - @EventHandler(ignoreCancelled = true) - public void onInventoryClick(InventoryClickEvent event) { - updateActivity((Player) event.getWhoClicked()); + private void updateListeners() { + if (andedProviders.isEmpty() && oredProviders.isEmpty()) { + defaultProvider.enableListeners(); + } else { + defaultProvider.disableListeners(); } } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 905d072..56af457 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -19,17 +19,20 @@ night-skip: exclusions: ignored-permission: true # Exclude players with the permission "harbor.ignored" from the sleeping count + exclude-survival: false # Exclude players in survival mode from the sleeping count exclude-adventure: false # Exclude players in adventure mode from the sleeping count exclude-creative: false # Exclude players in creative mode from the sleeping count - exclude-spectator: false # Exclude players in spectator mode from the sleeping count - exclude-vanished: false # Exclude vanished players from the sleeping count + exclude-spectator: true # Exclude players in spectator mode from the sleeping count + exclude-vanished: true # Exclude vanished players from the sleeping count exclude-afk: true # Exclude players who are considered afk from the sleeping count # Detect AFK players and automatically remove them from the required sleeping count # Essentials API is used for AFK detection when available- otherwise a fallback system is used afk-detection: - enabled: true - timeout: 15 # Time in minutes until a player is considered AFK + fallback-enabled: true + essentials-enabled: true + essentials-detection-mode: and # Plugins providing an AFK status, such as Essentials, can either have that AFK check ANDed or ORed with other plugin's checks. By default, we use ANDed detection (Essentials AND any other plugins must report the player as AFK) + fallback-timeout: 15 # Time in minutes until a player is considered AFK # Blacklist for worlds- Harbor will ignore these worlds blacklisted-worlds: @@ -68,7 +71,7 @@ messages: unrecognized-command: "Unrecognized command." # Spooky internal controls -version: 1.6.3 +version: 1.6.4 interval: 1 metrics: true debug: false