diff --git a/config.yml b/config.yml new file mode 100644 index 0000000..dabb470 --- /dev/null +++ b/config.yml @@ -0,0 +1,42 @@ +# firstJoin is the message that gets sent to a client, if it tries to connect when the server has a login queue. +# %d is the queue position. Remember to include %d somewhere in the string, but only once, otherwise errors will occur. +# +# toFastJoin is the message that gets sent to a client, if the client joins before minimumjointime has elapsed. +# there is no %d in this strng. Only text. +# +# updateMessage is the message that gets sent to a client, when it connects after having being added to the queue. +# %d is the queue position. Remember to include %d somewhere in the string, but only once, otherwise errors will occur. +# +# BanMessage is the message that gets sent to a client if the client is on the ban list. +# there is no %d in this strng. Only text. +messages: + firstJoin: The server is full, you have been added to the login queue. Your current position is %d. Please try again in no less than 10 seconds, and no more than 60 seconds. + toFastJoin: You rejoined way to quick, you have been moved to the end of the login queue. Next time please join no less than every 10 seconds. + updateMessage: Your position in the queue is %d. Please try again in no less than 10 seconds, and no more than 60 seconds. + banMessage: You are banned. If you believe this is a mistake, message modmail at www.reddit.com/r/civcraft +# all timers are in seconds. +timers: + # + # minimumJoinTime is the amount of time that must at least elapse, before a player tries to rejoin. + # If minimumJoinTime has not elapsed before they rejoin, they get removed from the login queue. + # + minimumJoinTime: 5 + # + # timeOutTime is the amount of time that can elapse, since a player last tried to join the server, + # before they are removed from the login queue. + # + timeOutTime: 60 +other: + initialDynCap: 175 +# +# whiteListedPlayers: a list of players that can join imediatetly, even if there is a login queue. +# Even though case is ignored in the code. Ie would recomend getting the case right. +# Because of config files being weird there needs to be something after the colon. +# However the right side of the colon is never read, so just make it something like the username again. +# +whiteListedPlayers: + ttk2: ttk2 + hammond_of_texas: hammond_of_texas + ariehkovler: ariehkovler + spock_bot: spock_bot + orthzar: orthzar \ No newline at end of file diff --git a/dyncap.jar b/dyncap.jar new file mode 100644 index 0000000..b168455 Binary files /dev/null and b/dyncap.jar differ diff --git a/plugin.yml b/plugin.yml new file mode 100644 index 0000000..4a3978f --- /dev/null +++ b/plugin.yml @@ -0,0 +1,48 @@ +name: DynCap +version: 1.0.1 +main: com.untamedears.DynCap.DynCapPlugin +author: Exultant +commands: + setcap: + description: Sets the player cap + usage: /setcap + permission: dyncap.console + getcap: + description: Displays the current player cap + usage: /getcap + permission: dyncap.console + getqueuesize: + description: Displays the current player queue size + usage: /getqueuesize + aliases: gqs + reloadqueue: + description: Reload the config.yml of dyncap + usage: /reloadQueue + permission: dyncap.console + getqueueinfo: + description: Displays information about the supplied queue item. + usage: /getqueueinfo optional if first paramter is a index + aliases: gqi + permission: dyncap.debug.getqueueinfo + getjoinaverage: + description: Gets the average amount of time spent in the login queue, before joining. + usage: /getjoinaverage + aliases: gja + permission: dyncap.console + resetjoinaverage: + description: Resets the average amount of time + usage: /resetjoinaverage + aliases: rja + permission: dyncap.console +permissions: + dyncap.console: + description: Console commands for DynCap + default: false + children: + dyncap.debug.getqueueinfo: true + dyncap.debug: + description: Debug commands for DynCap + default: false + dyncap.debug.getqueueinfo: + description: Get queue info command for DynCap + default: false \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..1a4643f --- /dev/null +++ b/pom.xml @@ -0,0 +1,46 @@ + + 4.0.0 + com.untamedears + DynCap + jar + 1.0.1-SNAPSHOT + DynCap + https://github.com/erocs/DynCap + + + 1.7 + 1.7 + + + + ${basedir}/src + + + + ${basedir} + + *.yml + + + + + + + + org.bukkit + bukkit + 1.7.2-R0.3 + provided + + + + + + bukkit-repo + http://repo.bukkit.org/content/groups/public/ + + + diff --git a/src/com/untamedears/DynCap/DynCapCommands.java b/src/com/untamedears/DynCap/DynCapCommands.java index 15c22bc..54bb511 100644 --- a/src/com/untamedears/DynCap/DynCapCommands.java +++ b/src/com/untamedears/DynCap/DynCapCommands.java @@ -5,6 +5,7 @@ import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; import org.bukkit.entity.Player; public class DynCapCommands implements CommandExecutor { @@ -19,14 +20,28 @@ public DynCapCommands(DynCapPlugin p, Logger l) { @Override public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) { - //commands can only be issued from the console + + if (label.equalsIgnoreCase("getQueueSize") || label.equalsIgnoreCase("gqs")) { + return getQueueSize(sender); + } else if (label.equalsIgnoreCase("getqueueinfo") || label.equalsIgnoreCase("gqi")) { + return getQueueInfo(sender, args); + } + + //commands below here can only be issued from the console if (sender instanceof Player) { return false; } - if (label.equalsIgnoreCase("setcap")) { + + if (label.equalsIgnoreCase("setCap")) { return setcapCmd(args); - } else if (label.equalsIgnoreCase("getcap")) { + } else if (label.equalsIgnoreCase("getCap")) { return getcapCmd(); + } else if (label.equalsIgnoreCase("reloadQueue")) { + return reloadQueueConfig(); + } else if (label.equalsIgnoreCase("getjoinaverage") || label.equalsIgnoreCase("gja")) { + return getJoinAverage(); + } else if (label.equalsIgnoreCase("resetjoinaverage") || label.equalsIgnoreCase("rja")) { + return resetJoinAverage(); } return false; } @@ -54,5 +69,128 @@ private boolean getcapCmd() { log.info(message); return true; } + + private boolean getQueueSize(CommandSender sender) + { + Integer queueSize = plugin.getQueueSize(); + return sendMessage(sender, queueSize + " players are in the queue."); + } + + private boolean reloadQueueConfig() + { + plugin.reloadConfig(); + plugin.initConfig(); + log.info("queue config reloaded"); + return true; + } + + private boolean getQueueInfo(CommandSender sender, String[] args) + { + if (!sender.hasPermission("dyncap.debug.getqueueinfo")) + { + return false; + } + if (args.length < 1 || args.length > 2) { return false; } + + if (args.length == 1) + { + //display info on 1 slot + if (isInteger(args[0])) + { + int startIndex = (Integer.parseInt(args[0]) - 1); + QueueItem queueItem = plugin.getQueueItem(startIndex); + if (queueItem != null) + { + return sendMessage(sender, startIndex + " " + queueItem.getName() + " " + queueItem.getSecondsSinceLastAttempt()); + } + else + { + return sendMessage(sender, "Could not find information about index " + args[0]); + } + } + //search by name + else + { + String playerName = args[0]; + int index = plugin.getQueuePosition(playerName); + if (index != -1) + { + QueueItem queueItem = plugin.getQueueItem(index); + return sendMessage(sender, index + " " + queueItem.getName() + " " + queueItem.getSecondsSinceLastAttempt() ); + } + else + { + return sendMessage(sender, "Could not find " + playerName + " in the queue."); + } + } + } + else if (args.length == 2) + { + if (isInteger(args[0]) && isInteger(args[1])) + { + int startIndex = (Integer.parseInt(args[0]) - 1); + int endIndex = (Integer.parseInt(args[1]) - 1); + for (int x = startIndex; x <= endIndex; x++) + { + QueueItem queueItem = plugin.getQueueItem(x); + if (queueItem != null) + { + sendMessage(sender, x + " " + queueItem.getName() + " " + queueItem.getSecondsSinceLastAttempt() ); + } + else + { + sendMessage(sender, "Could not find information about index " + (x + 1)); + } + } + return true; + } + else + { + sendMessage(sender, "Please enter two integers above 0"); + } + } + return false; + } + + private static boolean isInteger(String s) { + try { + Integer.parseInt(s); + } catch(NumberFormatException e) { + return false; + } + // only got here if we didn't return false + return true; + } + + + private boolean resetJoinAverage() + { + plugin.resetAverageTimeToJoin(); + log.info("Average join time has been reset!"); + return true; + } + + private boolean getJoinAverage() + { + float average = plugin.getAverageTimeToJoin(); + log.info("Average join time is " + average + " seconds."); + return true; + } + + private boolean sendMessage(CommandSender sender, String string) + { + if (sender instanceof Player) + { + Player player = (Player) sender; + player.sendMessage(string); + return true; + } + else if (sender instanceof ConsoleCommandSender) + { + log.info(string); + return true; + } + return false; + } } diff --git a/src/com/untamedears/DynCap/DynCapPlugin.java b/src/com/untamedears/DynCap/DynCapPlugin.java index ccba676..1966847 100644 --- a/src/com/untamedears/DynCap/DynCapPlugin.java +++ b/src/com/untamedears/DynCap/DynCapPlugin.java @@ -1,23 +1,34 @@ package com.untamedears.DynCap; +import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.logging.Logger; import org.bukkit.Bukkit; -import org.bukkit.World; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.player.PlayerKickEvent; -import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.player.AsyncPlayerPreLoginEvent; +import org.bukkit.event.server.ServerListPingEvent; import org.bukkit.plugin.java.JavaPlugin; public class DynCapPlugin extends JavaPlugin implements Listener { private DynCapCommands commands; - private int dynamicPlayerCap = 1000; + private int dynamicPlayerCap = 175; private Logger log; - private boolean whiteListEnabled = false; + private float averageTimeToJoin = 0F; + private int numberOfJoins = 0; + private List loginQueue = new ArrayList(); + private String firstJoinMessage; + private String updateMessage; + private String toFastJoinMessage; + private String banMessage; + private int minimumJoinTime; + private int timeOutTime; + private Set whiteListedPlayers; public void onEnable() { log = this.getLogger(); @@ -27,70 +38,234 @@ public void onEnable() { for (String command : getDescription().getCommands().keySet()) { getCommand(command).setExecutor(commands); } - } - - public void onDisable() { + + // Give the console permission + ConsoleCommandSender console = getServer().getConsoleSender(); + console.addAttachment(this, "dyncap.console", true); + + this.saveDefaultConfig(); + + initConfig(); + this.getServer().getScheduler().scheduleSyncRepeatingTask(this, new Runnable() + { + @Override + public void run() + { + removeOldQueueItems(timeOutTime); + }}, 0L, 100); } + + public void onDisable() {} - @EventHandler(priority=EventPriority.MONITOR) - public void onPlayerJoinEvent(PlayerJoinEvent event) { - updatePlayerCap(); - } - - @EventHandler(priority=EventPriority.MONITOR) - public void onPlayerQuitEvent(PlayerQuitEvent event) { - updatePlayerCap(); + public void initConfig() + { + FileConfiguration config = getConfig(); + firstJoinMessage = config.getString("messages.firstJoin", "The server is full, you have been added to the login queue. Your current position is %d. Please try again in no less than 10 seconds, and no more than 60 seconds."); + toFastJoinMessage = config.getString("messages.toFastJoin", "You rejoined way to quick, you have been moved to the end of the login queue. Next time please join no less than every 10 seconds."); + updateMessage = config.getString("messages.updateMessage", "Your posistion in the queue is %d. Please try again in no less than 10 seconds, and no more than 60 seconds."); + banMessage = config.getString("messages.banMessage", "You are banned. If you believe this is a mistake, message modmail at www.reddit.com/r/civcraft"); + minimumJoinTime = config.getInt("timers.minimumJoinTime", 5); + timeOutTime = config.getInt("timers.timeOutTime", 60); + dynamicPlayerCap = config.getInt("other.initialDynCap", 175); + whiteListedPlayers = config.getConfigurationSection("whiteListedPlayers").getKeys(false); } - @EventHandler(priority=EventPriority.MONITOR) - public void onPlayerKickEvent(PlayerKickEvent event) { - updatePlayerCap(); + @EventHandler + public void onServerListPingEvent(ServerListPingEvent event) + { + event.setMaxPlayers(getPlayerCap()); } - + public void setPlayerCap(int cap) { dynamicPlayerCap = cap; - updatePlayerCap(); + //updatePlayerCap(getPlayerCount()); } - + public int getPlayerCap() { return dynamicPlayerCap; } + + public int getPlayerCount() { + return this.getServer().getOnlinePlayers().length; + } - private void updatePlayerCap() { - int playerCount = getPlayerCount(); - if (playerCount >= dynamicPlayerCap) { - setWhitelist(true); - } else if (playerCount < dynamicPlayerCap) { - setWhitelist(false); + public int getQueueSize() + { + return loginQueue.size(); + } + + public synchronized QueueItem getQueueItem(int queueIndex) + { + if (queueIndex < 0 || queueIndex >= loginQueue.size()) + { + return null; + } + else + { + return loginQueue.get(queueIndex); + } + } + + @EventHandler(priority=EventPriority.LOWEST, ignoreCancelled = false) + public void onAsyncPlayerPreLoginEvent(AsyncPlayerPreLoginEvent event) + { + try + { + //log.info("login event called!"); + String playerName = event.getName().toLowerCase(); + + //if the player is whitelisted(admin/mod) + if (whiteListedPlayers.contains(playerName)) + { + event.allow(); + return; + } + if(this.getServer().getBannedPlayers().contains(this.getServer().getOfflinePlayer(event.getName()))) + { + event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_BANNED, banMessage); + return; + } + + int position = getQueuePosition(playerName); + //log.info("posistion is:" + position); + //if the server is not full, and there is no queue + if ((getPlayerCount() < getPlayerCap() && loginQueue.isEmpty())) + { + //log.info("allowed " + playerName + " to join, server is not full and has no queue!"); + event.allow(); + updateAverageTimeToJoin(0); + return; + } + //server is either full, or has a queue + else + { + //if the server has a queue, but there is enough space for the player + if(position != -1 && position + 1 <= getPlayerCap() - getPlayerCount()) + { + //log.info("allowed " + playerName + " to join, server is not full and he is in a queue!"); + event.allow(); + updateAverageTimeToJoin(loginQueue.get(position).getSecondsSinceFirstAttempt()); + removeFromQueue(position); + return; + } + //if the server has a queue, and there is not enough space for the player + else if (position != -1) + { + if (getQueueItem(position).getSecondsSinceLastAttempt() <= minimumJoinTime) + { + removeFromQueue(position); + event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_FULL, toFastJoinMessage); + return; + } + else + { + getQueueItem(position).updateDate(); + event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_FULL, String.format(updateMessage, (position + 1))); + return; + } + } + else + { + if ((!loginQueue.isEmpty() && loginQueue.size() <= (getPlayerCap() - getPlayerCount()))) + { + //if for some reason the person is in the queue remove them + if (position != -1) + { + removeFromQueue(position); + } + updateAverageTimeToJoin(0); + event.allow(); + return; + } + //log.info("disallowed " + playerName + " added him to queue"); + QueueItem queueItem = new QueueItem(playerName); + addToQueue(queueItem); + event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_FULL, String.format(firstJoinMessage, loginQueue.size())); + return; + } + } + } + catch (Exception e) + { + e.printStackTrace(); } } - private void setWhitelist(boolean enabled) { - Bukkit.setWhitelist(enabled); - Integer playerCount = getPlayerCount(); - Integer cap = getPlayerCap(); - String message = playerCount.toString()+"/"+cap.toString()+" players online "; - if (enabled && !whiteListEnabled) { - message+=" dynamic cap enabled."; - log.info(message); - } else if (!enabled && whiteListEnabled) { - message+=" dynamic cap disabled."; - log.info(message); + private synchronized void removeFromQueue(int index) + { + loginQueue.remove(index); + } + private synchronized void addToQueue(QueueItem queueItem) + { + loginQueue.add(queueItem); + } + //returning -1 means error/not contained + public synchronized int getQueuePosition(String name) + { + if (loginQueue.isEmpty()) + { + //log.info("queue is empty!"); + return -1; + } + for (int x = 0; x < loginQueue.size(); x++) + { + //log.info("x is:" + x + " and queueItem name is " + queue.get(x).getName() + " while paramter is " + name); + if (getQueueItem(x).getName().equalsIgnoreCase(name)) + { + return x; + } } - whiteListEnabled = enabled; + return -1; + } + + + public void updateAverageTimeToJoin(int newJoinTime) + { + numberOfJoins++; + Float tempFloat = Integer.valueOf(numberOfJoins).floatValue(); + Float tempFloat2 = Integer.valueOf(newJoinTime).floatValue(); + averageTimeToJoin = ((tempFloat -1F)/tempFloat) * averageTimeToJoin + (tempFloat2/tempFloat); } - public int getPlayerCount() { - int playerCount = 0; - - List worlds = this.getServer().getWorlds(); - for (World world : worlds) { - if (world != null) { - playerCount += world.getPlayers().size(); + public void resetAverageTimeToJoin() + { + averageTimeToJoin = 0F; + numberOfJoins = 0; + } + + public float getAverageTimeToJoin() + { + return averageTimeToJoin; + } + + //timeOut is in seconds + private synchronized void removeOldQueueItems(int timeOut) + { + if (loginQueue.isEmpty()) + { + return; + } + for (int x = 0; x< loginQueue.size(); x++) + { + if (loginQueue.get(x).getSecondsSinceLastAttempt() > timeOut) + { + removeFromQueue(x); + x -- ; } } - - return playerCount; } + + private void moveQueueItem(int oldIndex, int newIndex) + { + if (oldIndex < 0 || oldIndex >= loginQueue.size() || newIndex < 0 || newIndex >= loginQueue.size()) + { + return; + } + QueueItem tempItem = loginQueue.get(oldIndex); + loginQueue.remove(oldIndex); + loginQueue.add(newIndex, tempItem); + } + } + diff --git a/src/com/untamedears/DynCap/QueueItem.java b/src/com/untamedears/DynCap/QueueItem.java new file mode 100644 index 0000000..8a2bce0 --- /dev/null +++ b/src/com/untamedears/DynCap/QueueItem.java @@ -0,0 +1,40 @@ +package com.untamedears.DynCap; + +import java.util.Date; +import java.util.concurrent.TimeUnit; + +public class QueueItem +{ + private Date firstAttempt; + private Date lastAttempt; + private String name; + + public QueueItem(String playerName) + { + lastAttempt = new Date(); + firstAttempt = new Date(); + name = playerName; + } + public Date getLastAttempt() + { + return lastAttempt; + } + public int getSecondsSinceLastAttempt() + { + Date now = new Date(); + return (int) TimeUnit.MILLISECONDS.toSeconds(now.getTime() - lastAttempt.getTime()); + } + public int getSecondsSinceFirstAttempt() + { + Date now = new Date(); + return (int) TimeUnit.MILLISECONDS.toSeconds(now.getTime() - firstAttempt.getTime()); + } + public void updateDate() + { + lastAttempt = new Date(); + } + public String getName() + { + return name; + } +}