package com.sk89q.commandbook; import com.sk89q.commandbook.session.PersistentSession; import com.sk89q.commandbook.session.SessionComponent; import com.sk89q.commandbook.util.LocationUtil; import com.sk89q.commandbook.util.entity.player.PlayerUtil; import com.sk89q.minecraft.util.commands.Command; import com.sk89q.minecraft.util.commands.CommandContext; import com.sk89q.minecraft.util.commands.CommandException; import com.sk89q.minecraft.util.commands.CommandPermissions; import com.zachsthings.libcomponents.ComponentInformation; import com.zachsthings.libcomponents.Depend; import com.zachsthings.libcomponents.InjectComponent; import com.zachsthings.libcomponents.bukkit.BukkitComponent; import com.zachsthings.libcomponents.config.ConfigurationBase; import com.zachsthings.libcomponents.config.Setting; import org.bukkit.ChatColor; import org.bukkit.Server; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.entity.EntityTargetEvent; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryCloseEvent; import org.bukkit.event.inventory.InventoryOpenEvent; import org.bukkit.event.player.*; import java.util.Collection; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; @ComponentInformation(friendlyName = "AFK Checker", desc = "AFK Checking and management.") @Depend(components = {GodComponent.class, SessionComponent.class}) public class AFKComponent extends BukkitComponent implements Runnable, Listener { private final CommandBook inst = CommandBook.inst(); private final Logger log = CommandBook.logger(); private final Server server = CommandBook.server(); @InjectComponent private GodComponent godComp; @InjectComponent private SessionComponent sessions; private LocalConfiguration config; @Override public void enable() { config = configure(new LocalConfiguration()); CommandBook.registerEvents(this); if (config.useMovementEvent) { CommandBook.registerEvents(new MovementListener()); } registerCommands(Commands.class); server.getScheduler().runTaskTimer(inst, this, 20, 20); } @Override public void reload() { super.reload(); configure(config); } private static class LocalConfiguration extends ConfigurationBase { @Setting("use-movement-event") public boolean useMovementEvent = true; @Setting("movement-threshold") public double movementThreshold = .04; @Setting("sneak-movement-threshold") public double sneakMovementThreshold = .004; @Setting("afk-minutes") public int afkMinutes = 3; @Setting("afk-kick-minutes") public int afkKickMinutes = 60; @Setting("afk-general-sleep-ignored") public boolean afkGeneralSleepIgnored = true; @Setting("afk-command-sleep-ignored") public boolean afkCommandSleepIgnored = false; @Setting("afk-general-protection") public boolean afkGeneralProtection = true; @Setting("afk-command-protection") public boolean afkCommandProtection = false; @Setting("npc-compatibility-mode") public boolean npcCompatibilty = false; } /** * Determines if a player is marked as AFK. * * @param player the player to check * @return true if the player should be kicked */ public boolean isAfk(Player player) { return sessions.getSession(AFKSession.class, player).isAFK(); } /** * Determines if a time is sufficient to be AFK. * * @param time the last update time * @return true if the player should be kicked */ public boolean isAfk(long time) { return time != 0 && System.currentTimeMillis() - time >= TimeUnit.MINUTES.toMillis(config.afkMinutes); } /** * Determines if a player should be kicked for being AFK. * * @param player the player to check * @return true if the player should be kicked */ public boolean shouldKick(Player player) { return shouldKick(sessions.getSession(AFKSession.class, player).getLastUpdate()); } /** * Determines if a time is sufficient to be kicked for being AFK. * * @param time the last update time * @return true if the player should be kicked */ public boolean shouldKick(long time) { if (config.afkKickMinutes < 1) return false; double maxP = server.getMaxPlayers(); double curP = server.getOnlinePlayers().size(); double fraction = ((maxP - curP) + maxP * .2) / maxP; int duration = (int) Math.max(config.afkMinutes + 2, Math.min(config.afkKickMinutes, config.afkKickMinutes * fraction)); return time != 0 && System.currentTimeMillis() - time >= TimeUnit.MINUTES.toMillis(duration); } /** * Determines if a player can be set to ignored for the sleep check. * * @param player the player to check * @return true if the player can be set to ignored for the sleep check */ public boolean canIgnoreSleep(Player player) { AFKSession session = sessions.getSession(AFKSession.class, player); return (session.isRequested() && canIgnoreSleep(true)) || (isAfk(session.getLastUpdate()) && canIgnoreSleep(false)); } /** * Determines if a player can be set to ignored for the sleep check. * * @param requested whether or not the player requested to be afk * @return true if the player can be set to ignored for the sleep check */ public boolean canIgnoreSleep(boolean requested) { return !requested && config.afkGeneralSleepIgnored || requested && config.afkCommandSleepIgnored; } /** * Determines if a player can be protected. * * @param player the player to check * @return true if the player can be protected */ public boolean canProtect(Player player) { AFKSession session = sessions.getSession(AFKSession.class, player); return (session.isRequested() && canProtect(true)) || (isAfk(session.getLastUpdate()) && canProtect(false)); } /** * Determines if a player can be protected. * * @param requested whether or not the player requested to be afk * @return true if the player can be protected */ public boolean canProtect(boolean requested) { return !requested && config.afkGeneralProtection || requested && config.afkCommandProtection; } /** * Updates a player's last active time. * * @param player player to update */ public void update(Player player) { AFKSession session = sessions.getSession(AFKSession.class, player); // Update the time and remove the idle message session.setLastUpdate(System.currentTimeMillis()); session.setIdleStatus(null); // Restore god mode setting if (godComp != null && session.isProtected() && godComp.hasGodMode(player)) { godComp.disableGodMode(player); session.setProtected(false); } } @Override public void run() { Collection<? extends Player> onlinePlayers = config.npcCompatibilty ? server.getOnlinePlayers() : null; for (final AFKSession session : sessions.getSessions(AFKSession.class).values()) { if (session == null) continue; final Player target = session.getPlayer(); if (target == null || !session.getPlayer().isValid()) continue; if (onlinePlayers != null && (target.hasMetadata("NPC") || !onlinePlayers.contains(target))) { continue; } boolean passedTime = isAfk(session.getLastUpdate()); if (session.isRequested() || passedTime) { if (shouldKick(session.getLastUpdate())) { // Restore sleep ignored setting if (session.isSleepIgnored()) { target.setSleepingIgnored(false); session.setSleepIgnored(false); } server.getScheduler().runTaskLater(inst, new Runnable() { @Override public void run() { target.kickPlayer("Inactivity - " + (System.currentTimeMillis() - session.getLastUpdate()) / 60000 + " Minutes"); } }, 1); } else if (!session.isAFK()) { // Grey out list name String name = target.getName(); session.setLastTabName(target.getPlayerListName()); target.setPlayerListName(ChatColor.GRAY + name.substring(0, Math.min(14, name.length()))); // Mark the player as AFK session.setAFK(true); target.sendMessage(ChatColor.YELLOW + "You are now marked as AFK."); } // Check and set sleep ignored if (canIgnoreSleep(session.isRequested()) || (passedTime && canIgnoreSleep(false))) { if (!target.isSleepingIgnored()) { target.setSleepingIgnored(true); session.setSleepIgnored(true); } } // Check and set god mode if (godComp != null && (canProtect(session.isRequested()) || (passedTime && canProtect(false)))) { if (!godComp.hasGodMode(target)) { godComp.enableGodMode(target); session.setProtected(true); } } } else if (session.isAFK()) { // Fix list name String lastName = session.getLastTabName(); target.setPlayerListName(lastName == null ? target.getName() : lastName); // Restore sleep ignored setting if (session.isSleepIgnored()) { target.setSleepingIgnored(false); session.setSleepIgnored(false); } // Mark the player as not AFK session.setAFK(false); target.sendMessage(ChatColor.YELLOW + "You are no longer marked as AFK."); } } } public class Commands { @Command(aliases = {"afk", "away"}, usage = "", desc = "Set yourself as away", flags = "", min = 0, max = -1) @CommandPermissions({"commandbook.away"}) public void afk(CommandContext args, CommandSender sender) throws CommandException { Player player = PlayerUtil.checkPlayer(sender); String status = ""; if (args.argsLength() > 0) { status = args.getJoinedStrings(0); // Make sure someone isn't playing around with this if (status.equals("null")) { status = ""; } } sessions.getSession(AFKSession.class, player).setIdleStatus(status); player.sendMessage(ChatColor.YELLOW + (status.isEmpty() ? "Set as away" : "Set away status to \"" + status + "\"") + "."); } } @EventHandler public void onEntityTargetPlayer(EntityTargetEvent event) { if (event.getTarget() instanceof Player) { if (canProtect((Player) event.getTarget())) { event.setCancelled(true); } } } @EventHandler public void onPlayerJoin(PlayerJoinEvent event) { update(event.getPlayer()); } @EventHandler public void onPlayerChat(AsyncPlayerChatEvent event) { update(event.getPlayer()); } @EventHandler public void onCommand(PlayerCommandPreprocessEvent event) { update(event.getPlayer()); } @EventHandler public void onPlayerInteract(PlayerInteractEvent event) { update(event.getPlayer()); } @EventHandler public void onPlayerFish(PlayerFishEvent event) { update(event.getPlayer()); } @EventHandler public void onInventoryOpen(InventoryOpenEvent event) { if (event.getPlayer() instanceof Player) update((Player) event.getPlayer()); } @EventHandler public void onInventoryClick(InventoryClickEvent event) { if (event.getWhoClicked() instanceof Player) update((Player) event.getWhoClicked()); } @EventHandler public void onInventoryClose(InventoryCloseEvent event) { if (event.getPlayer() instanceof Player) update((Player) event.getPlayer()); } @EventHandler public void onEntityDamageEntityEvent(EntityDamageByEntityEvent event) { if (event.getDamager() instanceof Player) update((Player) event.getDamager()); } public class MovementListener implements Listener { @EventHandler public void onMoveChange(PlayerMoveEvent event) { Player player = event.getPlayer(); double distanceSQ = LocationUtil.distanceSquared2D(event.getFrom(), event.getTo()); if (distanceSQ > config.movementThreshold || (player.isSneaking() && distanceSQ > config.sneakMovementThreshold)) { update(player); } } } // AFK Session public static class AFKSession extends PersistentSession { @Setting("idle-status") private String idleStatus = "null"; private String lastTabName; private long lastUpdate = 0; private boolean protect = false; private boolean sleepIgnored = false; private boolean awayFromKeyboard = false; protected AFKSession() { super(THIRTY_MINUTES); } public Player getPlayer() { CommandSender sender = super.getOwner(); return sender instanceof Player ? (Player) sender : null; } public String getIdleStatus() { return idleStatus.equals("null") ? "" : idleStatus; } public void setIdleStatus(String status) { this.idleStatus = status == null ? "null" : status; } public String getLastTabName() { return lastTabName; } public void setLastTabName(String lastTabName) { this.lastTabName = lastTabName; } public boolean isRequested() { return !idleStatus.equals("null"); } public long getLastUpdate() { return lastUpdate; } public void setLastUpdate(long lastUpdate) { this.lastUpdate = lastUpdate; } public boolean isProtected() { return protect; } public void setProtected(boolean protect) { this.protect = protect; } public boolean isSleepIgnored() { return sleepIgnored; } public void setSleepIgnored(boolean sleepIgnored) { this.sleepIgnored = sleepIgnored; } public boolean isAFK() { return awayFromKeyboard; } public void setAFK(boolean awayFromKeyboard) { this.awayFromKeyboard = awayFromKeyboard; } } }