1
0
mirror of https://github.com/nkomarn/harbor.git synced 2024-11-27 20:35:21 +01:00

Feature: API for AFK and excluded players (#88)

* remove afk listener and move to own class

* implement afk listener with proper afk check

* use instant instead of millis

* Creation of interface to allow external plugins to exclude players

* Addition of better Javadoc

* Move Essentials checks; Add example Provider classes

* Quickfix: style

* wippy wip

* Expand AFK and Exclusion APIs to allow control of logic flow + more

Also change examples and remove stafffacilities

* Major cleanup

* Clean imports

* Add a comment

* Modernize vanished check and clean up ternary

* Cleanup ternary and fix typos that caused bugs

* Remove some terrible code I wrote without thinking

* misunderstanding

* A few minor fixes and cleanups

* Fix listeners to monitor priority

* Requested changes

* Fix bugs

* take out the trash :)

Remove things I thought I removed but didn't

Co-authored-by: mail@chojo.de <mail@chojo.de>
This commit is contained in:
Andrew Katz 2021-07-09 19:22:22 -06:00 committed by GitHub
parent f27335e242
commit 5aaa0398bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 549 additions and 117 deletions

View File

@ -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<Essentials> getEssentials() {
return Optional.ofNullable(essentials);

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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());
}
}

View File

@ -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<AfkPlayer> 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<Player, AfkPlayer>) 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();
}
}
}

View File

@ -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;
}
}

View File

@ -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<UUID, Instant> 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;
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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<ExclusionProvider> providers;
private final Harbor harbor;
private final Set<UUID> 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);
}
}

View File

@ -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) {

View File

@ -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<UUID, Long> cooldowns;
private final Map<UUID, Long> playerActivity;
private final Map<UUID, Instant> cooldowns;
private final Set<AFKProvider> andedProviders;
private final Set<AFKProvider> 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> 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();
}
}
}

View File

@ -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