- Some code cleanup

- measure serverside lag in a seperate thread, once per second
- (Server) lag resistant "more packets" check
- minor adjustments to default config settings
- fixed a bug that would deactivate the durability check if the
morepackets check got deactivated
This commit is contained in:
Evenprime 2011-09-03 19:05:59 +02:00
parent c0a124ac61
commit 5b56fc7302
19 changed files with 156 additions and 270 deletions

View File

@ -3,7 +3,7 @@ name: NoCheat
author: Evenprime
main: cc.co.evenprime.bukkit.nocheat.NoCheat
version: 2.01a
version: 2.01b
permissions:

View File

@ -117,8 +117,8 @@ public class DefaultConfiguration {
morePacketsNode.add(actions);
actions.add(0, "morepacketsLow moveCancel");
actions.add(15, "morepacketsMed moveCancel");
actions.add(30, "morepacketsHigh moveCancel");
actions.add(30, "morepacketsMed moveCancel");
actions.add(60, "morepacketsHigh moveCancel");
}
/**** MOVING.NOCLIP ****/

View File

@ -36,6 +36,12 @@ public class NoCheat extends JavaPlugin {
private BlockPlaceEventManager eventBlockPlaceManager;
private PlayerInteractEventManager eventPlayerInteractManager;
private int taskId = -1;
private int ingameseconds = 0;
private long lastIngamesecondTime = 0L;
private long lastIngamesecondDuration = 0L;
private boolean skipCheck = false;
private ActionManager action;
public NoCheat() {
@ -44,6 +50,10 @@ public class NoCheat extends JavaPlugin {
public void onDisable() {
if(taskId != -1) {
this.getServer().getScheduler().cancelTask(taskId);
taskId = -1;
}
PluginDescriptionFile pdfFile = this.getDescription();
if(conf != null)
@ -71,6 +81,24 @@ public class NoCheat extends JavaPlugin {
PluginDescriptionFile pdfFile = this.getDescription();
if(taskId == -1) {
taskId = this.getServer().getScheduler().scheduleSyncRepeatingTask(this, new Runnable() {
@Override
public void run() {
// If the previous second took to long, skip checks during this second
skipCheck = lastIngamesecondDuration > 1500;
long time = System.currentTimeMillis();
lastIngamesecondDuration = time - lastIngamesecondTime;
if(lastIngamesecondDuration < 1000) lastIngamesecondDuration = 1000;
lastIngamesecondTime = time;
ingameseconds++;
}
}, 0, 20);
}
log.logToConsole(LogLevel.LOW, "[NoCheat] version [" + pdfFile.getVersion() + "] is enabled.");
}
@ -89,4 +117,16 @@ public class NoCheat extends JavaPlugin {
public ActionManager getActionManager() {
return action;
}
public int getIngameSeconds() {
return ingameseconds;
}
public long getIngameSecondDuration() {
return lastIngamesecondDuration;
}
public boolean skipCheck() {
return skipCheck;
}
}

View File

@ -9,11 +9,12 @@ package cc.co.evenprime.bukkit.nocheat;
public class Permissions {
private final static String _NOCHEAT = "nocheat";
private final static String _ADMIN = _NOCHEAT + ".admin";
private final static String _CHECKS = _NOCHEAT + ".checks";
private final static String _MOVE = _CHECKS + ".moving";
private final static String _BLOCKBREAK = _CHECKS + ".blockbreak";
public static final String _BLOCKPLACE = _CHECKS + ".blockplace";
public final static String _INTERACT = _CHECKS + ".interact";
private final static String _BLOCKPLACE = _CHECKS + ".blockplace";
private final static String _INTERACT = _CHECKS + ".interact";
public final static String MOVE = _CHECKS + ".moving.*";
public final static String MOVE_FLY = _MOVE + ".flying";
@ -34,7 +35,6 @@ public class Permissions {
public final static String BLOCKPLACE_ONLIQUID = _BLOCKPLACE + ".onliquid";
public static final String BLOCKPLACE_REACH = _BLOCKPLACE + ".reach";
private final static String _ADMIN = _NOCHEAT + ".admin";
public final static String ADMIN_CHATLOG = _ADMIN + ".chatlog";

View File

@ -19,7 +19,7 @@ import org.bukkit.plugin.Plugin;
*/
public class ConsoleCommandSender implements CommandSender {
private Server server;
private final Server server;
private final PermissibleBase perm = new PermissibleBase(this);
public ConsoleCommandSender(Server server) {

View File

@ -21,7 +21,7 @@ import cc.co.evenprime.bukkit.nocheat.data.BlockBreakData;
*/
public class ReachCheck {
private ActionExecutor action;
private final ActionExecutor action;
public ReachCheck(NoCheat plugin) {
this.action = new ActionExecutorWithHistory(plugin);

View File

@ -19,7 +19,7 @@ import cc.co.evenprime.bukkit.nocheat.data.BlockPlaceData;
*/
public class OnLiquidCheck {
private ActionExecutor action;
private final ActionExecutor action;
public OnLiquidCheck(NoCheat plugin) {
action = new ActionExecutorWithHistory(plugin);

View File

@ -24,7 +24,7 @@ import cc.co.evenprime.bukkit.nocheat.data.BlockPlaceData;
*/
public class ReachCheck {
private ActionExecutor action;
private final ActionExecutor action;
public ReachCheck(NoCheat plugin) {
this.action = new ActionExecutorWithHistory(plugin);

View File

@ -29,7 +29,7 @@ public class InteractCheck {
boolean cancel = false;
final boolean durability = cc.moving.morePacketsCheck && !player.hasPermission(Permissions.INTERACT_DURABILITY);
final boolean durability = cc.interact.durabilityCheck && !player.hasPermission(Permissions.INTERACT_DURABILITY);
if(durability) {
// It's so simple, I'll just do the check in place

View File

@ -6,7 +6,6 @@ import org.bukkit.Location;
import org.bukkit.entity.Player;
import cc.co.evenprime.bukkit.nocheat.NoCheat;
import cc.co.evenprime.bukkit.nocheat.Permissions;
import cc.co.evenprime.bukkit.nocheat.actions.ActionExecutor;
import cc.co.evenprime.bukkit.nocheat.actions.ActionExecutorWithHistory;
import cc.co.evenprime.bukkit.nocheat.actions.types.LogAction;
@ -29,21 +28,71 @@ public class MorePacketsCheck {
private final ActionExecutor action;
private final long timeframe = 1000;
private final double packetsPerTimeframe = 22;
private final double lowLimit = -20;
private final long packetsPerTimeframe = 22;
private final double bufferLimit = 30;
private final NoCheat plugin;
public MorePacketsCheck(NoCheat plugin) {
this.action = new ActionExecutorWithHistory(plugin);
this.plugin = plugin;
}
/**
* Ok, lets write down the internal logic, to not forget what I wanted to
* do when I started it:
*
* A second on the server has 20 ticks
* A second on the client has 20 ticks
* A unmodified client with no lag will send 1 packet per tick = 20 packets
* Ideally checking for 20 packets/second on the server would work
*
* But a client may send 10 packets in second 1, and 30 packets in second 2
* due to lag. This should still be allowed, therefore we give a "buffer".
* If a player doesn't use all of his 20 move packets in one second, he may
* carry over unused packets to the next (the buffer is limited in size for
* obvious reasons).
*
* But the server may not be able to process all packets in time, e.g.
* because it is busy saving the world.
*
* Well that sounded strange...
* Admin: "Hey server, what is taking you so long?"
* Server: "I'm saving the world!"
* Admin: "o_O"
*
* Contrary to client lag, serverside lag could be measured. So let's do
* that. A task will be executed every 20 server ticks, storing the time it
* took the server to process those ticks. If it's much more than 1 second,
* the server was busy, and packets may have piled up during that time. So a
* good idea would be to ignore the following second completely, as it will
* be used to process the stacked-up packets, getting the server back in
* sync with the timeline.
*
* Server identified as being busy -> ignore the second that follows. If
* that second the server is still busy -> ignore the second after that too.
*
* What's with the second during which the server is busy? Can there be more
* packets during that time? Sure. But should we care? No, this initial lag
* can be mitigated by using the time it took to do the 20 ticks and factor
* it with the limit for packets. Problem solved. The only real problem are
* packets that stack up in one second to get processed in the next, which
* is what the "ignoring" is for.
*
* So the general course of action would be:
*
* 1. Collect packets processed within 20 server ticks = packetCounter
* 2. Measure time taken for those 20 server ticks = elapsedTime
* 3. elapsedTime >> 1 second -> ignore next check
* 4. limit = 22 x elapsedTime
* 5. difference = limit - packetCounter
* 6. buffer = buffer + difference; if(buffer > 20) buffer = 20;
* 7. if(buffer < 0) -> violation of size "buffer".
* 8. reset packetCounter, wait for next 20 ticks to pass by.
*
*/
public Location check(Player player, ConfigurationCache cc, MovingData data) {
if(!cc.moving.morePacketsCheck || player.hasPermission(Permissions.MOVE_MOREPACKETS)) {
return null;
}
Location newToLocation = null;
data.morePacketsCounter++;
@ -51,46 +100,58 @@ public class MorePacketsCheck {
data.morePacketsSetbackPoint = player.getLocation();
}
long currentTime = System.currentTimeMillis();
// Is at least half a second gone by?
if(currentTime - timeframe > data.morePacketsLastTime) {
int ingameSeconds = plugin.getIngameSeconds();
// Is at least a second gone by and has the server at least processed 20
// ticks since last time
if(ingameSeconds != data.lastElapsedIngameSeconds) {
// Are we over the 10 event limit for that time frame now?
final double change = data.morePacketsCounter - packetsPerTimeframe * ((double) (currentTime - data.morePacketsLastTime)) / ((double) timeframe);
long limit = (packetsPerTimeframe * plugin.getIngameSecondDuration()) / 1000L;
if(change > 0) {
data.morePacketsOverLimit += change;
} else if(data.morePacketsOverLimit + change > lowLimit) {
data.morePacketsOverLimit += change;
} else if(data.morePacketsOverLimit > lowLimit) {
data.morePacketsOverLimit = lowLimit;
}
long difference = limit - data.morePacketsCounter;
if(data.morePacketsOverLimit > 0 && data.morePacketsCounter > packetsPerTimeframe) {
data.morePacketsBuffer = data.morePacketsBuffer + difference;
if(data.morePacketsBuffer > bufferLimit)
data.morePacketsBuffer = bufferLimit;
// Are we over the 22 event limit for that time frame now? (limit
// increases with time)
int packetsAboveLimit = (int) -data.morePacketsBuffer;
if(data.morePacketsBuffer < 0)
data.morePacketsBuffer = 0;
// Should we react? Only if the check doesn't get skipped and we
// went over the limit
if(!plugin.skipCheck() && packetsAboveLimit > 0) {
data.morePacketsViolationLevel += packetsAboveLimit;
HashMap<String, String> params = new HashMap<String, String>();
params.put(LogAction.PACKETS, String.valueOf(data.morePacketsCounter - packetsPerTimeframe));
// Packets above limit
params.put(LogAction.PACKETS, String.valueOf(data.morePacketsCounter - limit));
params.put(LogAction.CHECK, "morepackets");
boolean cancel = false;
cancel = action.executeActions(player, cc.moving.morePacketsActions, (int) data.morePacketsOverLimit, params, cc);
cancel = action.executeActions(player, cc.moving.morePacketsActions, (int) data.morePacketsViolationLevel, params, cc);
if(cancel) {
newToLocation = data.morePacketsSetbackPoint != null ? data.morePacketsSetbackPoint : player.getLocation();
// Only do the cancel if the player didn't change worlds
// inbetween
if(cancel && player.getWorld().equals(data.morePacketsSetbackPoint.getWorld())) {
newToLocation = data.morePacketsSetbackPoint;
}
}
// No new setbackLocation was chosen
if(newToLocation == null) {
data.morePacketsSetbackPoint = player.getLocation();
}
if(data.morePacketsOverLimit > 0)
data.morePacketsOverLimit *= 0.8; // Shrink the "over limit"
// value by 20 % every second
data.morePacketsLastTime = currentTime;
data.morePacketsCounter = 0;
if(data.morePacketsViolationLevel > 0)
// Shrink the "over limit" value by 20 % every second
data.morePacketsViolationLevel *= 0.8;
data.morePacketsCounter = 0; // Count from zero again
data.lastElapsedIngameSeconds = ingameSeconds;
}
return newToLocation;

View File

@ -165,7 +165,7 @@ public class MovingEventHelper {
return((value & NONSOLID) == NONSOLID);
}
public final boolean isLadder(int value) {
private final boolean isLadder(int value) {
return((value & LADDER) == LADDER);
}

View File

@ -43,8 +43,6 @@ public class ConfigurationManager {
private final static String defaultActionFileName = "default_actions.txt";
private final static String descriptionsFileName = "descriptions.txt";
public final static String rootConfigFolder = "plugins/NoCheat/"; // default
private final Map<String, ConfigurationCache> worldnameToConfigCacheMap = new HashMap<String, ConfigurationCache>();
// Only use one filehandler per file, therefore keep open filehandlers in a
@ -96,7 +94,7 @@ public class ConfigurationManager {
}
public void initializeActions(String rootConfigFolder, ActionManager action) {
private void initializeActions(String rootConfigFolder, ActionManager action) {
FlatActionParser parser = new FlatActionParser();

View File

@ -8,7 +8,7 @@ package cc.co.evenprime.bukkit.nocheat.config.tree;
*/
public class ActionOption extends ChildOption implements Comparable<ActionOption> {
private int treshold;
private final int treshold;
private String value;
public ActionOption(Integer treshold, String value) {
@ -30,23 +30,6 @@ public class ActionOption extends ChildOption implements Comparable<ActionOption
return true;
}
/**
* It is recommended to take further action if the treshold
* gets changed: If successful, "clone()" this object and
* replace the original with it.
*
* @param treshold
* @return
*/
public boolean setTreshold(String treshold) {
try {
this.treshold = Integer.parseInt(treshold);
return true;
} catch(NumberFormatException e) {
return false;
}
}
public int getTreshold() {
return treshold;
}

View File

@ -19,7 +19,7 @@ public class DataManager {
private final Map<Player, MovingData> movingData = new HashMap<Player, MovingData>();
private final Map<Player, BlockBreakData> blockbreakData = new HashMap<Player, BlockBreakData>();
private final Map<Player, InteractData> interactData = new HashMap<Player, InteractData>();
private Map<Player, BlockPlaceData> blockPlaceData = new HashMap<Player, BlockPlaceData>();
private final Map<Player, BlockPlaceData> blockPlaceData = new HashMap<Player, BlockPlaceData>();
public DataManager() {

View File

@ -29,12 +29,12 @@ public class MovingData {
public double horizontalBuffer;
public int morePacketsCounter;
public long morePacketsLastTime;
public double morePacketsOverLimit = -50;
public double morePacketsBuffer = 50;
public Location morePacketsSetbackPoint;
public double morePacketsViolationLevel = 0;
public Location teleportTo;
public int lastElapsedIngameSeconds = 0;
}

View File

@ -58,7 +58,6 @@ public class PlayerTeleportEventManager extends PlayerListener {
@Override
public void onPlayerTeleport(PlayerTeleportEvent event) {
if(!event.isCancelled())
handleTeleportation(event.getPlayer(), event.getTo());
@ -73,7 +72,7 @@ public class PlayerTeleportEventManager extends PlayerListener {
handleTeleportation(event.getPlayer(), event.getRespawnLocation());
}
public void handleTeleportation(Player player, Location newLocation) {
private void handleTeleportation(Player player, Location newLocation) {
/********* Moving check ************/
final MovingData data = this.data.getMovingData(player);

View File

@ -10,6 +10,7 @@ import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import cc.co.evenprime.bukkit.nocheat.DefaultConfiguration;
import cc.co.evenprime.bukkit.nocheat.actions.ActionManager;
/**
@ -35,7 +36,7 @@ public class FlatActionParser {
List<String[]> lines = new LinkedList<String[]>();
if(!file.exists()) {
createActionFile(file);
DefaultConfiguration.writeActionFile(file);
return lines;
}
@ -69,22 +70,4 @@ public class FlatActionParser {
return lines;
}
public void createActionFile(File file) {
if(!file.exists()) {
try {
file.createNewFile();
BufferedWriter writer = new BufferedWriter(new FileWriter(file));
writer.write("# Use this file to define your own actions.");
writer.newLine();
writer.write("# Look at \"default_actions.txt\" for inspiration.");
writer.newLine();
writer.flush();
writer.close();
} catch(Exception e) {
e.printStackTrace();
};
}
}
}

View File

@ -1,58 +0,0 @@
package cc.co.evenprime.bukkit.nocheat.file;
import cc.co.evenprime.bukkit.nocheat.config.tree.ActionListOption;
import cc.co.evenprime.bukkit.nocheat.config.tree.ActionOption;
import cc.co.evenprime.bukkit.nocheat.config.tree.ChildOption;
import cc.co.evenprime.bukkit.nocheat.config.tree.ConfigurationTree;
import cc.co.evenprime.bukkit.nocheat.config.tree.Option;
import cc.co.evenprime.bukkit.nocheat.config.tree.ParentOption;
/**
* An extremely simple YAML configuration generator
*
* @author Evenprime
*
*/
public class YamlConfigGenerator {
private final static String spaces = " ";
public static String treeToYaml(ConfigurationTree tree) {
ParentOption o = (ParentOption) tree.getOption("");
String s = "";
for(Option option : o.getChildOptions()) {
s += optionToYamlString(option, "");
}
return s;
}
private static String optionToYamlString(Option option, String prefix) {
String s = "";
if(option instanceof ParentOption) {
s += prefix + option.getIdentifier() + ":\r\n";
prefix += spaces;
for(Option o : ((ParentOption) option).getChildOptions()) {
s += optionToYamlString(o, prefix);
}
} else if(option instanceof ActionListOption && option.isActive()) {
s += prefix + option.getIdentifier() + ":\r\n";
for(ActionOption o : ((ActionListOption) option).getChildOptions()) {
s += prefix + spaces + o.getIdentifier() + ": \"" + o.getStringValue() + "\"\r\n";
}
} else if(option instanceof ChildOption && option.isActive()) {
s += prefix + option.getIdentifier() + ": \"" + ((ChildOption) option).getStringValue() + "\"\r\n";
}
return s;
}
}

View File

@ -1,120 +0,0 @@
package cc.co.evenprime.bukkit.nocheat.file;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
/**
* An extremely simple YAML config parser
*
* @author Evenprime
*
*/
public class YamlConfigParser {
private final String prefix = " ";
private final Map<String, Object> root;
public YamlConfigParser() {
root = new HashMap<String, Object>();
}
public void read(File file) throws IOException {
BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"));
LinkedList<String> lines = new LinkedList<String>();
String line = null;
while((line = r.readLine()) != null) {
lines.add(line);
}
r.close();
parse(root, lines, "");
}
private void parse(Map<String, Object> root, LinkedList<String> lines, String prefix) throws IOException {
String line = null;
while(!lines.isEmpty()) {
line = lines.getFirst();
if(line.trim().startsWith("#")) {
lines.removeFirst();
} else if(line.trim().isEmpty()) {
lines.removeFirst();
} else if(line.startsWith(prefix)) {
lines.removeFirst();
if(line.contains(":")) {
String pair[] = line.split(":", 2);
if(pair[1].trim().isEmpty()) {
Map<String, Object> m = new HashMap<String, Object>();
parse(m, lines, prefix + this.prefix);
root.put(pair[0].trim(), m);
} else {
root.put(pair[0].trim(), removeQuotationMarks(pair[1].trim()));
}
}
} else
break;
}
}
private static String removeQuotationMarks(String s) {
if(s.startsWith("\"") && s.endsWith("\"")) {
return s.substring(1, s.length() - 1);
} else if(s.startsWith("\'") && s.endsWith("\'")) {
return s.substring(1, s.length() - 1);
}
return s;
}
/* Convenience methods for retrieving values start here */
public Object getProperty(String path) {
return getProperty(path, root);
}
@SuppressWarnings("unchecked")
private static Object getProperty(String path, Map<String, Object> node) {
if(node == null) {
return null;
}
if(!path.contains(".")) {
return node.get(path);
} else {
String[] parts = path.split("\\.", 2);
return getProperty(parts[1], (Map<String, Object>) node.get(parts[0]));
}
}
public String getString(String path, String defaultValue) {
return getString(path, defaultValue, root);
}
private static String getString(String path, String defaultValue, Map<String, Object> node) {
try {
String result = (String) getProperty(path, node);
if(result == null)
return defaultValue;
return result;
} catch(Exception e) {
return defaultValue;
}
}
}