diff --git a/src/fr/neatmonster/nocheatplus/checks/chat/ChatConfig.java b/src/fr/neatmonster/nocheatplus/checks/chat/ChatConfig.java index 99a23f02..d7f1b1dd 100644 --- a/src/fr/neatmonster/nocheatplus/checks/chat/ChatConfig.java +++ b/src/fr/neatmonster/nocheatplus/checks/chat/ChatConfig.java @@ -71,11 +71,14 @@ public class ChatConfig implements CheckConfig { public final ActionList colorActions; public final boolean globalChatCheck; - public final boolean globalChatEngineCheck; public final EnginePlayerConfig globalChatEnginePlayerConfig; public final float globalChatFrequencyFactor; public final float globalChatFrequencyWeight; + public final float globalChatGlobalWeight; + public final float globalChatPlayerWeight; public final double globalChatLevel; + public boolean globalChatEngineMaximum; + public final boolean globalChatDebug; public final ActionList globalChatActions; public final boolean noPwnageCheck; @@ -145,11 +148,14 @@ public class ChatConfig implements CheckConfig { colorActions = config.getActionList(ConfPaths.CHAT_COLOR_ACTIONS, Permissions.CHAT_COLOR); globalChatCheck = config.getBoolean(ConfPaths.CHAT_GLOBALCHAT_CHECK); - globalChatEngineCheck = config.getBoolean(ConfPaths.CHAT_GLOBALCHAT_ENGINE_CHECK); globalChatEnginePlayerConfig = new EnginePlayerConfig(config); globalChatFrequencyFactor = (float) config.getDouble(ConfPaths.CHAT_GLOBALCHAT_FREQUENCY_FACTOR); globalChatFrequencyWeight = (float) config.getDouble(ConfPaths.CHAT_GLOBALCHAT_FREQUENCY_WEIGHT); + globalChatGlobalWeight = (float) config.getDouble(ConfPaths.CHAT_GLOBALCHAT_GL_WEIGHT, 1.0); + globalChatPlayerWeight = (float) config.getDouble(ConfPaths.CHAT_GLOBALCHAT_PP_WEIGHT, 1.0); globalChatLevel = config.getDouble(ConfPaths.CHAT_GLOBALCHAT_LEVEL); + globalChatEngineMaximum = config.getBoolean(ConfPaths.CHAT_GLOBALCHAT_ENGINE_MAXIMUM, true); + globalChatDebug = config.getBoolean(ConfPaths.CHAT_GLOBALCHAT_DEBUG, false); globalChatActions = config.getActionList(ConfPaths.CHAT_GLOBALCHAT_ACTIONS, Permissions.CHAT_GLOBALCHAT); noPwnageCheck = config.getBoolean(ConfPaths.CHAT_NOPWNAGE_CHECK); diff --git a/src/fr/neatmonster/nocheatplus/checks/chat/GlobalChat.java b/src/fr/neatmonster/nocheatplus/checks/chat/GlobalChat.java index 4e91dc76..32bb1ba3 100644 --- a/src/fr/neatmonster/nocheatplus/checks/chat/GlobalChat.java +++ b/src/fr/neatmonster/nocheatplus/checks/chat/GlobalChat.java @@ -1,5 +1,10 @@ package fr.neatmonster.nocheatplus.checks.chat; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + import org.bukkit.entity.Player; import fr.neatmonster.nocheatplus.checks.Check; @@ -11,6 +16,7 @@ import fr.neatmonster.nocheatplus.command.INotifyReload; import fr.neatmonster.nocheatplus.config.ConfigFile; import fr.neatmonster.nocheatplus.config.ConfigManager; import fr.neatmonster.nocheatplus.hooks.NCPExemptionManager; +import fr.neatmonster.nocheatplus.utilities.CheckUtils; /** * Some alternative more or less advanced analysis methods. @@ -48,7 +54,7 @@ public class GlobalChat extends Check implements INotifyReload{ synchronized (data) { return unsafeCheck(player, message, captcha, cc, data, isMainThread); - } + } } private void init() { @@ -89,6 +95,15 @@ public class GlobalChat extends Check implements INotifyReload{ boolean cancel = false; + boolean debug = cc.globalChatDebug; + + final List debugParts; + if (debug){ + debugParts = new LinkedList(); + debugParts.add("[NoCheatPlus][globalchat] Message ("+player.getName()+"/"+message.length()+"): "); + } + else debugParts = null; + // Update the frequency interval weights. data.globalChatFrequency.update(time); @@ -152,22 +167,28 @@ public class GlobalChat extends Check implements INotifyReload{ wWords /= (float) letterCounts.words.length; score += wWords; + if (debug && score > 0f) debugParts.add("Simple score: " + CheckUtils.fdec3.format(score)); + // Engine: - if (cc.globalChatEngineCheck){ - final float wEngine; - synchronized (engine) { - wEngine = engine.process(letterCounts, player.getName(), cc, data); - } - score += wEngine; + // TODO: more fine grained sync ! + float wEngine = 0f; + final Map engMap; + synchronized (engine) { + engMap = engine.process(letterCounts, player.getName(), cc, data); + // TODO: more fine grained sync !s + // TODO: different methods (add or max or add+max or something else). + for (final Float res : engMap.values()){ + if (cc.globalChatEngineMaximum) wEngine = Math.max(wEngine, res.floatValue()); + else wEngine += res.floatValue(); + } } + score += wEngine; // Wrapping it up. -------------------- // Add weight to frequency counts. data.globalChatFrequency.add(time, score); final float accumulated = cc.globalChatFrequencyWeight * data.globalChatFrequency.getScore(cc.globalChatFrequencyFactor); -// System.out.println("Total score: " + score + " (" + accumulated + ")"); - if (score < 2.0f * cc.globalChatFrequencyWeight) // Reset the VL. data.globalChatVL = 0.0; @@ -185,7 +206,21 @@ public class GlobalChat extends Check implements INotifyReload{ } else data.globalChatVL *= 0.95; - + + if (debug) { + final List keys = new LinkedList(engMap.keySet()); + Collections.sort(keys); + for (String key : keys) { + Float s = engMap.get(key); + if (s.floatValue() > 0.0f) + debugParts.add(key + ":" + CheckUtils.fdec3.format(s)); + } + if (wEngine > 0.0f) + debugParts.add("Engine score (" + (cc.globalChatEngineMaximum?"max":"sum") + "): " + CheckUtils.fdec3.format(wEngine)); + debugParts.add("Total score: " + CheckUtils.fdec3.format(score) + " (weigth=" + cc.globalChatFrequencyWeight + " => accumulated=" + CheckUtils.fdec3.format(accumulated) + ", vl=" + CheckUtils.fdec3.format(data.globalChatVL)); + CheckUtils.scheduleOutputJoined(debugParts, " | "); + debugParts.clear(); + } return cancel; } diff --git a/src/fr/neatmonster/nocheatplus/checks/chat/analysis/ds/bktree/BKLevenshtein.java b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/ds/bktree/BKLevenshtein.java new file mode 100644 index 00000000..f5776a43 --- /dev/null +++ b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/ds/bktree/BKLevenshtein.java @@ -0,0 +1,132 @@ +package fr.neatmonster.nocheatplus.checks.chat.analysis.ds.bktree; + +import fr.neatmonster.nocheatplus.checks.chat.analysis.ds.bktree.BKLevenshtein.LevenNode; +import fr.neatmonster.nocheatplus.checks.chat.analysis.ds.bktree.BKModTree.LookupEntry; + +/** + * Some version of a BK-Levenshtein tree. + * @author mc_dev + * + * @param + */ +public class BKLevenshtein, L extends LookupEntry> extends BKModTree { + + /** + * Fat default impl. + * @author mc_dev + * + * @param + */ + public static class LevenNode> extends HashMapNode{ + public LevenNode(char[] value) { + super(value); + } + } + + public BKLevenshtein(NodeFactory nodeFactory, LookupEntryFactory resultFactory) { + super(nodeFactory, resultFactory); + } + +// @Override +// public int distance(final char[] s1, final char[] s2) { +// /* +// * Levenshtein distance, also known as edit distance. +// * Not optimal implementation (m*n space). +// * Gusfield, Dan (1999), Algorithms on Strings, Sequences and Trees. Cambridge: University Press. +// * (2012/09/07) http://en.literateprograms.org/Levenshtein_distance_%28Java%29#chunk%20use:usage +// */ +// final int n = s1.length; +// final int m = s2.length; +// if (n == m){ +// // Bad style "fix", to return 0 on equality. +// int match = 0; +// for (int i = 0; i < n; i++){ +// if (s1[i] == s2[i]) match ++; +// else break; +// } +// if (match == m) return 0; +// } +// final int[][] dp = new int[n + 1][m + 1]; +// for (int i = 0; i < dp.length; i++) { +// for (int j = 0; j < dp[i].length; j++) { +// dp[i][j] = i == 0 ? j : j == 0 ? i : 0; +// if (i > 0 && j > 0) { +// if (s1[i - 1] == s2[j - 1]) +// dp[i][j] = dp[i - 1][j - 1]; +// else +// dp[i][j] = Math.min(dp[i][j - 1] + 1, Math.min( +// dp[i - 1][j - 1] + 1, dp[i - 1][j] + 1)); +// } +// } +// } +// return dp[n][m]; +// } + + @Override + public int distance(char[] s, char[] t) { + /* + * Adapted from CheckUtils to char[]. + * NOTE: RETURNS 1 FOR SAME STRINGS. + */ + // if (s == null || t == null) + // throw new IllegalArgumentException("Strings must not be null"); + + int n = s.length; + int m = t.length; + + if (n == m){ + // Return equality faster. + for (int i = 0; i < n; i++){ + if (s[i] == t[i]) m --; + else break; + } + if (m == 0) return 0; + m = n; // Reset. + } + + if (n == 0) + return m; + else if (m == 0) + return n; + + if (n > m) { + final char[] tmp = s; + s = t; + t = tmp; + n = m; + m = t.length; + } + + int p[] = new int[n + 1]; + int d[] = new int[n + 1]; + int _d[]; + + int i; + int j; + + char t_j; + + int cost; + + for (i = 0; i <= n; i++) + p[i] = i; + + for (j = 1; j <= m; j++) { + t_j = t[j - 1]; + d[0] = j; + + for (i = 1; i <= n; i++) { + cost = s[i - 1] == t_j ? 0 : 1; + d[i] = Math.min(Math.min(d[i - 1] + 1, p[i] + 1), p[i - 1] + + cost); + } + + _d = p; + p = d; + d = _d; + } + + return p[n]; + } + +} diff --git a/src/fr/neatmonster/nocheatplus/checks/chat/analysis/ds/bktree/BKModTree.java b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/ds/bktree/BKModTree.java new file mode 100644 index 00000000..a423f159 --- /dev/null +++ b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/ds/bktree/BKModTree.java @@ -0,0 +1,271 @@ +package fr.neatmonster.nocheatplus.checks.chat.analysis.ds.bktree; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import fr.neatmonster.nocheatplus.checks.chat.analysis.ds.bktree.BKModTree.LookupEntry; +import fr.neatmonster.nocheatplus.checks.chat.analysis.ds.bktree.BKModTree.Node; + +/** + * BK tree for int distances. + * @author mc_dev + * + */ +public abstract class BKModTree, L extends LookupEntry>{ + + // TODO: Support for other value (equals) than used for lookup (distance). + // TODO: What with dist = 0 -> support for exact hit ! + + /** + * Fat defaultimpl. it iterates over all Children + * @author mc_dev + * + * @param + * @param + */ + public static abstract class Node>{ + public V value; + + public Node(V value){ + this.value = value; + } + public abstract N putChild(final int distance, final N child); + + public abstract N getChild(final int distance); + + public abstract boolean hasChild(int distance); + + public abstract Collection getChildren(final int distance, final int range, final Collection nodes); + } + + /** + * Node using a map as base, with basic implementation. + * @author mc_dev + * + * @param + * @param + */ + public static abstract class MapNode> extends Node{ + protected Map children = null; // Only created if needed. + protected int maxIterate = 12; // Maybe add a setter. + public MapNode(V value) { + super(value); + } + @Override + public N putChild(final int distance, final N child){ + if (children == null) children = newMap(); + children.put(distance, child); + return child; + } + @Override + public N getChild(final int distance){ + if (children == null) return null; + return children.get(distance); + } + @Override + public boolean hasChild(int distance) { + if (children == null) return false; + return children.containsKey(distance); + } + @Override + public Collection getChildren(final int distance, final int range, final Collection nodes){ + if (children == null) return nodes; + // TODO: maybe just go for iterating till range (from 0 on) to have closest first (no keyset). + if (children.size() > maxIterate){ + for (int i = distance - range; i < distance + range + 1; i ++){ + final N child = children.get(i); + if (child != null) nodes.add(child); + } + } + else{ + for (final Integer key : children.keySet()){ + // TODO: Not sure this is faster than the EntrySet in average. + if (Math.abs(distance - key.intValue()) <= range) nodes.add(children.get(key)); + } + } + return nodes; + } + /** + * Map factory method. + * @return + */ + protected abstract Map newMap(); + } + + /** + * Node using a simple HashMap. + * @author mc_dev + * + * @param + * @param + */ + public static class HashMapNode> extends MapNode{ + /** Map Levenshtein distance to next nodes. */ + protected int initialCapacity = 4; + protected float loadFactor = 0.75f; + public HashMapNode(V value) { + super(value); + } + + @Override + protected Map newMap() { + return new HashMap(initialCapacity, loadFactor); + } + } + + public static class SimpleNode extends HashMapNode>{ + public SimpleNode(V content) { + super(content); + } + } + + public static interface NodeFactory>{ + public N newNode(V value, N parent); + } + + /** + * Result of a lookup. + * @author mc_dev + * + * @param + * @param + */ + public static class LookupEntry>{ + // TODO: What nodes are in nodes, actually? Those from the way that were in range ? + // TODO: This way one does not know which distance a node has. [subject to changes] + // TODO: Add depth and some useful info ? + + /** All visited nodes within range of distance. */ + public final Collection nodes; + /** Matching node */ + public final N match; + /** Distance from value to match.value */ + public final int distance; + /** If the node match is newly inserted.*/ + public final boolean isNew; + + public LookupEntry(Collection nodes, N match, int distance, boolean isNew){ + this.nodes = nodes; + this.match = match; + this.distance = distance; + this.isNew = isNew; + } + } + + public static interface LookupEntryFactory, L extends LookupEntry>{ + public L newLookupEntry(Collection nodes, N match, int distance, boolean isNew); + } + + protected final NodeFactory nodeFactory; + + protected final LookupEntryFactory resultFactory; + + protected N root = null; + + /** Set to true to have visit called */ + protected boolean visit = false; + + public BKModTree(NodeFactory nodeFactory, LookupEntryFactory resultFactory){ + this.nodeFactory = nodeFactory; + this.resultFactory = resultFactory; + } + + public void clear(){ + root = null; + } + + /** + * + * @param value + * @param range Maximum difference from distance of node.value to children. + * @param seekMax If node.value is within distance but not matching, this is the maximum number of steps to search on. + * @param create + * @return + */ + public L lookup(final V value, final int range, final int seekMax, final boolean create){ // TODO: signature. + final List inRange = new LinkedList(); + if (root == null){ + if (create){ + root = nodeFactory.newNode(value, null); + return resultFactory.newLookupEntry(inRange, root, 0, true); + } + else{ + return resultFactory.newLookupEntry(inRange, null, 0, false); + } + } + // TODO: best queue type. + final List open = new ArrayList(); + open.add(root); + N insertion = null; + int insertionDist = 0; + do{ + final N current = open.remove(open.size() - 1); + int distance = distance(current.value, value); + if (visit) visit(current, value, distance); + if (distance == 0){ + // exact match. + return resultFactory.newLookupEntry(inRange, current, distance, false); + } + // Set node as insertion point. + if (create && insertion == null && !current.hasChild(distance)){ + insertion = current; + insertionDist = distance; + // TODO: use + } + // Within range ? + if (Math.abs(distance) <= range){ + inRange.add(current); + // Check special abort conditions. + if (seekMax > 0 && inRange.size() >= seekMax){ + // TODO: Keep this ? + // Break if insertion point is found, or not needed. + if (!create || insertion != null){ + break; + } + } + } + // Continue search with children. + current.getChildren(distance, range, open); + + // TODO: deterministic: always same node visited for the same value ? [Not with children = HashMap...] + } while (!open.isEmpty()); + + // TODO: is the result supposed to be the closest match, if any ? + + if (create && insertion != null){ + final N newNode = nodeFactory.newNode(value, insertion); + insertion.putChild(insertionDist, newNode); + return resultFactory.newLookupEntry(inRange, newNode, 0, true); + } + else{ + return resultFactory.newLookupEntry(inRange, null, 0, false); + } + } + + /** + * Visit a node during lookup. + * @param node + * @param distance + * @param value + */ + protected void visit(N node, V value, int distance){ + // Override if needed. + } + + ////////////////////////////////////////////// + // Abstract methods. + ////////////////////////////////////////////// + + /** + * Calculate the distance of two values. + * @param v1 + * @param v2 + * @return + */ + public abstract int distance(V v1, V v2); + +} diff --git a/src/fr/neatmonster/nocheatplus/checks/chat/analysis/ds/bktree/SimpleTimedBKLevenshtein.java b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/ds/bktree/SimpleTimedBKLevenshtein.java new file mode 100644 index 00000000..a4500379 --- /dev/null +++ b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/ds/bktree/SimpleTimedBKLevenshtein.java @@ -0,0 +1,35 @@ +package fr.neatmonster.nocheatplus.checks.chat.analysis.ds.bktree; + +import java.util.Collection; + +import fr.neatmonster.nocheatplus.checks.chat.analysis.ds.bktree.BKModTree.LookupEntry; +import fr.neatmonster.nocheatplus.checks.chat.analysis.ds.bktree.SimpleTimedBKLevenshtein.STBKLResult; +import fr.neatmonster.nocheatplus.checks.chat.analysis.ds.bktree.TimedBKLevenshtein.SimpleTimedLevenNode; + +public class SimpleTimedBKLevenshtein extends TimedBKLevenshtein { + + public static class STBKLResult extends LookupEntry{ + public STBKLResult(Collection nodes, SimpleTimedLevenNode match, int distance, boolean isNew) { + super(nodes, match, distance, isNew); + } + + } + + public SimpleTimedBKLevenshtein() { + super( + new NodeFactory(){ + @Override + public SimpleTimedLevenNode newNode( char[] value, SimpleTimedLevenNode parent) { + return new SimpleTimedLevenNode(value); + } + } + , + new LookupEntryFactory() { + @Override + public STBKLResult newLookupEntry(Collection nodes, SimpleTimedLevenNode match, int distance, boolean isNew) { + return new STBKLResult(nodes, match, distance, isNew); + } + } + ); + } +} diff --git a/src/fr/neatmonster/nocheatplus/checks/chat/analysis/ds/bktree/TimedBKLevenshtein.java b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/ds/bktree/TimedBKLevenshtein.java new file mode 100644 index 00000000..502ddbfe --- /dev/null +++ b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/ds/bktree/TimedBKLevenshtein.java @@ -0,0 +1,38 @@ +package fr.neatmonster.nocheatplus.checks.chat.analysis.ds.bktree; + +import fr.neatmonster.nocheatplus.checks.chat.analysis.ds.bktree.BKModTree.LookupEntry; +import fr.neatmonster.nocheatplus.checks.chat.analysis.ds.bktree.TimedBKLevenshtein.TimedLevenNode; + +public class TimedBKLevenshtein, L extends LookupEntry> extends BKLevenshtein { + + + public static class TimedLevenNode> extends LevenNode{ + public long ts; + /** + * Set time to now. + * @param value + */ + public TimedLevenNode(char[] value) { + super(value); + this.ts = System.currentTimeMillis(); + } + public TimedLevenNode(char[] value, long ts){ + super(value); + this.ts = ts; + } + } + + public static class SimpleTimedLevenNode extends TimedLevenNode{ + public SimpleTimedLevenNode(char[] value) { + super(value); + } + public SimpleTimedLevenNode(char[] value, long ts){ + super(value, ts); + } + } + + public TimedBKLevenshtein(NodeFactory nodeFactory, LookupEntryFactory resultFactory) { + super(nodeFactory, resultFactory); + } + +} diff --git a/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/CompressedWords.java b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/CompressedWords.java deleted file mode 100644 index 1910300c..00000000 --- a/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/CompressedWords.java +++ /dev/null @@ -1,99 +0,0 @@ -package fr.neatmonster.nocheatplus.checks.chat.analysis.engine; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import fr.neatmonster.nocheatplus.checks.chat.analysis.MessageLetterCount; -import fr.neatmonster.nocheatplus.checks.chat.analysis.WordLetterCount; -import fr.neatmonster.nocheatplus.checks.chat.analysis.ds.prefixtree.SimpleTimedCharPrefixTree; -import fr.neatmonster.nocheatplus.checks.chat.analysis.ds.prefixtree.SimpleTimedCharPrefixTree.SimpleTimedCharLookupEntry; - -public class CompressedWords extends AbstractWordProcessor{ - - protected final SimpleTimedCharPrefixTree tree = new SimpleTimedCharPrefixTree(true); - - protected final int maxAdd; - - protected int added = 0; - - protected final boolean sort; - - protected final long durExpire; - - protected final List letters = new ArrayList(10); - protected final List digits = new ArrayList(10); - protected final List other = new ArrayList(10); - - public CompressedWords(long durExpire, int maxAdd, boolean sort) { - super("CompressedWords"); - this.durExpire = durExpire; - this.maxAdd = maxAdd; - this.sort = sort; - } - - @Override - public void start(final MessageLetterCount message) { - // This allows adding up to maximum messge length more characters, - // but also allows to set size of nodes exactly. - // TODO: Some better method than blunt clear (extra LinkedHashSet/LRU?). - if (added > maxAdd) tree.clear(); - added = 0; - } - - @Override - public float loop(final long ts, final int index, final String key, final WordLetterCount word) { - final int len = word.counts.size(); - letters.clear(); - digits.clear(); - other.clear(); - for (Character c : word.counts.keySet()){ - if (Character.isLetter(c)) letters.add(c); - else if (Character.isDigit(c)) digits.add(c); - else other.add(c); - } - if (sort){ - Collections.sort(letters); - Collections.sort(digits); - Collections.sort(other); - } - float score = 0; - if (!letters.isEmpty()){ - score += getScore(letters, ts); - } - if (!digits.isEmpty()){ - score += getScore(digits, ts); - } - if (!other.isEmpty()){ - score += getScore(other, ts); - } - return word.counts.isEmpty()?0f:(score / (float) len); - } - - @Override - public void clear() { - tree.clear(); - letters.clear(); - digits.clear(); - other.clear(); - } - - protected float getScore(final List chars, final long ts) { - final int len = chars.size(); - final SimpleTimedCharLookupEntry entry = tree.lookup(chars, true); - final int depth = entry.depth; - float score = 0f; - for (int i = 0; i < depth ; i++){ - final long age = ts - entry.timeInsertion[i]; - if (age < durExpire) - score += 1f / (float) (depth - i) * (float) (durExpire - age) / (float) durExpire; - } - if (depth == len){ - score += 0.2; - if (entry.insertion.isEnd) score += 0.2; - } - if (len != depth) added += len - depth; - return score; - } - -} diff --git a/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/EnginePlayerConfig.java b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/EnginePlayerConfig.java index 538ff898..ce4d7463 100644 --- a/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/EnginePlayerConfig.java +++ b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/EnginePlayerConfig.java @@ -1,5 +1,8 @@ package fr.neatmonster.nocheatplus.checks.chat.analysis.engine; +import fr.neatmonster.nocheatplus.checks.chat.analysis.engine.processors.FlatWords.FlatWordsSettings; +import fr.neatmonster.nocheatplus.checks.chat.analysis.engine.processors.SimilarWordsBKL.SimilarWordsBKLSettings; +import fr.neatmonster.nocheatplus.checks.chat.analysis.engine.processors.WordPrefixes.WordPrefixesSettings; import fr.neatmonster.nocheatplus.config.ConfPaths; import fr.neatmonster.nocheatplus.config.ConfigFile; @@ -10,11 +13,35 @@ import fr.neatmonster.nocheatplus.config.ConfigFile; */ public class EnginePlayerConfig { - public final boolean ppComprWordsCheck; - public final boolean ppWordFrequencyCheck; + public final boolean ppPrefixesCheck; + public final WordPrefixesSettings ppPrefixesSettings; + public final boolean ppWordsCheck; + public final FlatWordsSettings ppWordsSettings; + public final boolean ppSimilarityCheck; + public final SimilarWordsBKLSettings ppSimilaritySettings; public EnginePlayerConfig(final ConfigFile config){ - ppWordFrequencyCheck = config.getBoolean(ConfPaths.CHAT_GLOBALCHAT_ENGINE_PPWORDFREQ_CHECK, false); - ppComprWordsCheck = config.getBoolean(ConfPaths.CHAT_GLOBALCHAT_ENGINE_PPCOMPRWORDS_CHECK, false); + // NOTE: These settings should be compared to the global settings done in the LetterEngine constructor. + ppWordsCheck = config.getBoolean(ConfPaths.CHAT_GLOBALCHAT_PP_WORDS_CHECK, false); + if (ppWordsCheck){ + ppWordsSettings = new FlatWordsSettings(); + ppWordsSettings.maxSize = 150; // Adapt to smaller size. + ppWordsSettings.applyConfig(config, ConfPaths.CHAT_GLOBALCHAT_PP_WORDS); + } + else ppWordsSettings = null; // spare some memory. + ppPrefixesCheck = config.getBoolean(ConfPaths.CHAT_GLOBALCHAT_PP_PREFIXES_CHECK, false); + if (ppPrefixesCheck){ + ppPrefixesSettings = new WordPrefixesSettings(); + ppPrefixesSettings.maxAdd = 320; // Adapt to smaller size. + ppPrefixesSettings.applyConfig(config, ConfPaths.CHAT_GLOBALCHAT_PP_PREFIXES); + } + else ppPrefixesSettings = null; + ppSimilarityCheck = config.getBoolean(ConfPaths.CHAT_GLOBALCHAT_PP_SIMILARITY_CHECK, false); + if (ppSimilarityCheck){ + ppSimilaritySettings = new SimilarWordsBKLSettings(); + ppSimilaritySettings.maxSize = 100; // Adapt to smaller size; + ppSimilaritySettings.applyConfig(config, ConfPaths.CHAT_GLOBALCHAT_PP_SIMILARITY); + } + else ppSimilaritySettings = null; } } diff --git a/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/EnginePlayerData.java b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/EnginePlayerData.java index df7f2c0a..6a8a3881 100644 --- a/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/EnginePlayerData.java +++ b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/EnginePlayerData.java @@ -4,6 +4,10 @@ import java.util.ArrayList; import java.util.List; import fr.neatmonster.nocheatplus.checks.chat.ChatConfig; +import fr.neatmonster.nocheatplus.checks.chat.analysis.engine.processors.SimilarWordsBKL; +import fr.neatmonster.nocheatplus.checks.chat.analysis.engine.processors.WordPrefixes; +import fr.neatmonster.nocheatplus.checks.chat.analysis.engine.processors.FlatWords; +import fr.neatmonster.nocheatplus.checks.chat.analysis.engine.processors.WordProcessor; /** * Engine specific player data. @@ -16,15 +20,12 @@ public class EnginePlayerData { public EnginePlayerData(ChatConfig cc) { EnginePlayerConfig config = cc.globalChatEnginePlayerConfig; - if (config.ppWordFrequencyCheck){ - // TODO: configure. - processors.add(new FlatWordBuckets(50, 4, 1500, 0.9f)); - } - if (config.ppComprWordsCheck){ - // TODO: configure. - processors.add(new CompressedWords(30000, 320, false)); - } - + if (config.ppWordsCheck) + processors.add(new FlatWords("ppWords", config.ppWordsSettings)); + if (config.ppPrefixesCheck) + processors.add(new WordPrefixes("ppPrefixes", config.ppPrefixesSettings)); + if (config.ppSimilarityCheck) + processors.add(new SimilarWordsBKL("ppSimilarity", config.ppSimilaritySettings)); } - + } diff --git a/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/EnginePlayerDataMap.java b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/EnginePlayerDataMap.java index ff721aa0..67083061 100644 --- a/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/EnginePlayerDataMap.java +++ b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/EnginePlayerDataMap.java @@ -2,6 +2,7 @@ package fr.neatmonster.nocheatplus.checks.chat.analysis.engine; import fr.neatmonster.nocheatplus.checks.chat.ChatConfig; import fr.neatmonster.nocheatplus.checks.chat.analysis.ds.ManagedMap; +import fr.neatmonster.nocheatplus.checks.chat.analysis.engine.processors.WordProcessor; /** * Store EnginePlayerData. Expire data on get(String, Chatonfig). @@ -32,7 +33,7 @@ public class EnginePlayerDataMap extends ManagedMap { put(key, data); } final long ts = System.currentTimeMillis(); - if (ts - durExpire > lastAccess) expire(ts - durExpire); + if (ts - lastAccess > durExpire) expire(ts - durExpire); lastAccess = ts; return data; } diff --git a/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/FlatWordBuckets.java b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/FlatWordBuckets.java deleted file mode 100644 index 1b998c25..00000000 --- a/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/FlatWordBuckets.java +++ /dev/null @@ -1,55 +0,0 @@ -package fr.neatmonster.nocheatplus.checks.chat.analysis.engine; - -import java.util.LinkedHashMap; - -import fr.neatmonster.nocheatplus.checks.chat.analysis.MessageLetterCount; -import fr.neatmonster.nocheatplus.checks.chat.analysis.WordLetterCount; -import fr.neatmonster.nocheatplus.utilities.ActionFrequency; - -/** - * Words mapped to ActionFrequency queues. - * @author mc_dev - * - */ -public class FlatWordBuckets extends AbstractWordProcessor{ - final int maxSize; - final LinkedHashMap entries; - final long durBucket; - final int nBuckets; - final float factor; - - public FlatWordBuckets(int maxSize, int nBuckets, long durBucket, float factor){ - super("FlatWordBuckets"); - this.maxSize = maxSize; - entries = new LinkedHashMap(maxSize); - this.nBuckets = nBuckets; - this.durBucket = durBucket; - this.factor = factor; - } - - @Override - public void start(final MessageLetterCount message) { - if (entries.size() + message.words.length > maxSize) - releaseMap(entries, Math.max(message.words.length, maxSize / 10)); - } - - @Override - public float loop(long ts, int index, String key, WordLetterCount word) { - ActionFrequency freq = entries.get(key); - if (freq == null){ - freq = new ActionFrequency(nBuckets, durBucket); - entries.put(key, freq); - return 0.0f; - } - freq.update(ts); - float score = Math.min(1.0f, freq.getScore(factor)); - freq.add(ts, 1.0f); - return score; - } - - @Override - public void clear() { - entries.clear(); - } - -} diff --git a/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/LetterEngine.java b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/LetterEngine.java index 753de4bd..fec2d64e 100644 --- a/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/LetterEngine.java +++ b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/LetterEngine.java @@ -1,11 +1,22 @@ package fr.neatmonster.nocheatplus.checks.chat.analysis.engine; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; + +import org.bukkit.Bukkit; import fr.neatmonster.nocheatplus.checks.chat.ChatConfig; import fr.neatmonster.nocheatplus.checks.chat.ChatData; import fr.neatmonster.nocheatplus.checks.chat.analysis.MessageLetterCount; +import fr.neatmonster.nocheatplus.checks.chat.analysis.engine.processors.FlatWords; +import fr.neatmonster.nocheatplus.checks.chat.analysis.engine.processors.FlatWords.FlatWordsSettings; +import fr.neatmonster.nocheatplus.checks.chat.analysis.engine.processors.SimilarWordsBKL; +import fr.neatmonster.nocheatplus.checks.chat.analysis.engine.processors.SimilarWordsBKL.SimilarWordsBKLSettings; +import fr.neatmonster.nocheatplus.checks.chat.analysis.engine.processors.WordPrefixes; +import fr.neatmonster.nocheatplus.checks.chat.analysis.engine.processors.WordPrefixes.WordPrefixesSettings; +import fr.neatmonster.nocheatplus.checks.chat.analysis.engine.processors.WordProcessor; import fr.neatmonster.nocheatplus.config.ConfPaths; import fr.neatmonster.nocheatplus.config.ConfigFile; @@ -17,50 +28,65 @@ import fr.neatmonster.nocheatplus.config.ConfigFile; */ public class LetterEngine { + /** Global processors */ protected final List processors = new ArrayList(); protected final EnginePlayerDataMap dataMap; public LetterEngine(ConfigFile config){ // Add word processors. - if (config.getBoolean(ConfPaths.CHAT_GLOBALCHAT_ENGINE_GLWORDFREQ_CHECK, false)){ - // TODO: Make aspects configurable. - processors.add(new FlatWordBuckets(1000, 4, 1500, 0.9f)); + // NOTE: These settings should be compared to the per player settings done in the EnginePlayerConfig constructor. + if (config.getBoolean(ConfPaths.CHAT_GLOBALCHAT_GL_WORDS_CHECK, false)){ + FlatWordsSettings settings = new FlatWordsSettings(); + settings.maxSize = 1000; + settings.applyConfig(config, ConfPaths.CHAT_GLOBALCHAT_GL_WORDS); + processors.add(new FlatWords("glWords",settings)); } - if (config.getBoolean(ConfPaths.CHAT_GLOBALCHAT_ENGINE_GLCOMPRWORDS_CHECK, false)){ - // TODO: Make aspects configurable. - processors.add(new CompressedWords(30000, 2000, false)); + if (config.getBoolean(ConfPaths.CHAT_GLOBALCHAT_GL_PREFIXES_CHECK , false)){ + WordPrefixesSettings settings = new WordPrefixesSettings(); + settings.maxAdd = 2000; + settings.applyConfig(config, ConfPaths.CHAT_GLOBALCHAT_GL_PREFIXES); + processors.add(new WordPrefixes("glPrefixes", settings)); } - - // TODO: At least expiration duration configurable? + if (config.getBoolean(ConfPaths.CHAT_GLOBALCHAT_GL_SIMILARITY_CHECK , false)){ + SimilarWordsBKLSettings settings = new SimilarWordsBKLSettings(); + settings.maxSize = 1000; + settings.applyConfig(config, ConfPaths.CHAT_GLOBALCHAT_GL_SIMILARITY); + processors.add(new SimilarWordsBKL("glSimilarity", settings)); + } + // TODO: At least expiration duration configurable? (Entries expire after 10 minutes.) dataMap = new EnginePlayerDataMap(600000L, 100, 0.75f); } - public float process(final MessageLetterCount letterCount, final String playerName, final ChatConfig cc, final ChatData data){ - float score = 0; + public Map process(final MessageLetterCount letterCount, final String playerName, final ChatConfig cc, final ChatData data){ + + final Map result = new HashMap(); // Global processors. for (final WordProcessor processor : processors){ - final float refScore = processor.process(letterCount); - -// System.out.println("global:" + processor.getProcessorName() +": " + refScore); - - score = Math.max(score, refScore); + try{ + result.put(processor.getProcessorName(), processor.process(letterCount) * cc.globalChatGlobalWeight); + } + catch( final Exception e){ + Bukkit.getLogger().warning("[NoCheatPlus] globalchat: processor("+processor.getProcessorName()+") generated an exception: " + e.getClass().getSimpleName() + ": " + e.getMessage()); + e.printStackTrace(); + continue; + } } // Per player processors. final EnginePlayerData engineData = dataMap.get(playerName, cc); for (final WordProcessor processor : engineData.processors){ - final float refScore = processor.process(letterCount); - -// System.out.println("player: " + processor.getProcessorName() +": " + refScore); - - score = Math.max(score, refScore); + try{ + result.put(processor.getProcessorName(), processor.process(letterCount) * cc.globalChatPlayerWeight); + } + catch( final Exception e){ + Bukkit.getLogger().warning("[NoCheatPlus] globalchat: processor("+processor.getProcessorName()+") generated an exception: " + e.getClass().getSimpleName() + ": " + e.getMessage()); + e.printStackTrace(); + continue; + } } - - // TODO: Is max the right method? - - return score; + return result; } public void clear() { diff --git a/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/AbstractWordProcessor.java b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/processors/AbstractWordProcessor.java similarity index 74% rename from src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/AbstractWordProcessor.java rename to src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/processors/AbstractWordProcessor.java index c5ab3e4b..141cba22 100644 --- a/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/AbstractWordProcessor.java +++ b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/processors/AbstractWordProcessor.java @@ -1,4 +1,4 @@ -package fr.neatmonster.nocheatplus.checks.chat.analysis.engine; +package fr.neatmonster.nocheatplus.checks.chat.analysis.engine.processors; import java.util.LinkedList; import java.util.List; @@ -28,6 +28,9 @@ public abstract class AbstractWordProcessor implements WordProcessor{ } protected String name; + /** Not set by constructor. */ + protected float weight = 1f; + public AbstractWordProcessor(String name){ this.name = name; } @@ -37,6 +40,17 @@ public abstract class AbstractWordProcessor implements WordProcessor{ return name; } + + + @Override + public float getWeight() { + return weight; + } + + public void setWeight(float weight){ + this.weight = weight; + } + @Override public float process(final MessageLetterCount message) { // Does the looping, scores are summed up and divided by number of words. @@ -46,9 +60,9 @@ public abstract class AbstractWordProcessor implements WordProcessor{ for (int index = 0; index < message.words.length; index++){ final WordLetterCount word = message.words[index]; final String key = word.word.toLowerCase(); - score += loop(ts, index, key, word); + score += loop(ts, index, key, word) * (float) (word.word.length() + 1); } - score /= (float) message.words.length; + score /= (float) (message.message.length() + message.words.length); return score; } @@ -65,7 +79,7 @@ public abstract class AbstractWordProcessor implements WordProcessor{ * Process one word. * @param index * @param message - * @return Score. + * @return Score, suggested to be within [0 .. 1]. */ public abstract float loop(final long ts, final int index, final String key, final WordLetterCount word); } diff --git a/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/processors/DigestedWords.java b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/processors/DigestedWords.java new file mode 100644 index 00000000..73ecad73 --- /dev/null +++ b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/processors/DigestedWords.java @@ -0,0 +1,149 @@ +package fr.neatmonster.nocheatplus.checks.chat.analysis.engine.processors; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import fr.neatmonster.nocheatplus.checks.chat.analysis.WordLetterCount; +import fr.neatmonster.nocheatplus.config.ConfigFile; + +public abstract class DigestedWords extends AbstractWordProcessor{ + + /** + * Doubling code a little for the sake of flexibility with config reading. + * @author mc_dev + * + */ + public static class DigestedWordsSettings{ + public boolean sort = false; + public boolean compress = false; + public boolean split = false; + public float weight = 1f; + + public DigestedWordsSettings(){ + } + + public DigestedWordsSettings(boolean sort, boolean compress, boolean split, float weight) { + this.sort = sort; + this.compress = compress; + this.split = split; + this.weight = weight; + } + + /** + * Returns this object. + * @param config + * @param prefix Prefix for direct addition of config path. + * @return This object. + */ + public DigestedWordsSettings applyConfig(ConfigFile config, String prefix){ + this.sort = config.getBoolean(prefix + "sort", this.sort); + this.compress = config.getBoolean(prefix + "compress", this.compress); + this.split = config.getBoolean(prefix + "split", this.split); + this.weight = (float) config.getDouble(prefix + "weight", this.weight); + return this; + } + } + + protected final boolean sort; + protected final boolean compress; + protected final boolean split; + + protected final List letters = new ArrayList(10); + protected final List digits = new ArrayList(10); + protected final List other = new ArrayList(10); + + /** + * Constructor for a given settings instance. + * @param name + * @param settings + */ + public DigestedWords(String name, DigestedWordsSettings settings){ + this(name, settings.sort, settings.compress, settings.split); + this.weight = settings.weight; + } + + /** + * + * @param durExpire + * @param maxAdd + * @param sort Sort letters. + * @param compress Only use every letter once. + * @param split Check for letters, digits, other individually (!). + */ + public DigestedWords(String name, boolean sort, boolean compress, boolean split) { + super(name); + this.sort = sort; + this.compress = compress; + this.split = split; + } + + @Override + public float loop(final long ts, final int index, final String key, final WordLetterCount word) { + letters.clear(); + digits.clear(); + other.clear(); + Collection chars; + if (compress) chars = word.counts.keySet(); + else{ + // Add all. + chars = new ArrayList(word.word.length()); + for (int i = 0; i < word.word.length(); i++){ + char c = word.word.charAt(i); + if (Character.isUpperCase(c)) c = Character.toLowerCase(c); + chars.add(c); + // hmm. Maybe provide all the lists in the WordLetterCount already. + } + } + final int len = chars.size(); + for (Character c : chars){ + if (!split || Character.isLetter(c)) letters.add(c); + else if (Character.isDigit(c)) digits.add(c); + else other.add(c); + } + if (sort){ + Collections.sort(letters); + Collections.sort(digits); + Collections.sort(other); + } + float score = 0; + if (!letters.isEmpty()){ + score += getScore(letters, ts) * (float) letters.size(); + } + if (!digits.isEmpty()){ + score += getScore(digits, ts) * (float) digits.size(); + } + if (!other.isEmpty()){ + score += getScore(other, ts) * (float) other.size(); + } + return len == 0?0f:(score / (float) len); + } + + @Override + public void clear() { + letters.clear(); + digits.clear(); + other.clear(); + } + + public static final char[] toArray(final Collection chars){ + final char[] a = new char[chars.size()]; + int i = 0; + for (final Character c : chars){ + a[i] = c; + i ++; + // TODO: lol, horrible. + } + return a; + } + + /** + * + * @param chars List of characters, should be lower case, could be split / compressed. + * @param ts common timestamp for processing the whole message. + * @return + */ + protected abstract float getScore(final List chars, final long ts); + +} diff --git a/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/processors/FlatWords.java b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/processors/FlatWords.java new file mode 100644 index 00000000..cdade30d --- /dev/null +++ b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/processors/FlatWords.java @@ -0,0 +1,99 @@ +package fr.neatmonster.nocheatplus.checks.chat.analysis.engine.processors; + +import java.util.LinkedHashMap; +import java.util.List; + +import fr.neatmonster.nocheatplus.checks.chat.analysis.MessageLetterCount; +import fr.neatmonster.nocheatplus.config.ConfigFile; +import fr.neatmonster.nocheatplus.utilities.ActionFrequency; + +/** + * Words mapped to ActionFrequency queues. + * @author mc_dev + * + */ +public class FlatWords extends DigestedWords{ + + public static class FlatWordsSettings extends DigestedWordsSettings{ + public int maxSize = 1000; + public long durBucket = 1500; + public int nBuckets = 4; + public float factor = 0.9f; + /** + * split by default. + */ + public FlatWordsSettings(){ + super(false, true, true, 1f); + } + public FlatWordsSettings (int maxSize, int nBuckets, long durBucket, float factor, boolean sort, boolean compress, boolean split, float weight){ + super(sort, compress, split, weight); + this.maxSize = maxSize; + this.nBuckets = nBuckets; + this.durBucket = durBucket; + this.factor = factor; + } + public FlatWordsSettings applyConfig(ConfigFile config, String prefix){ + super.applyConfig(config, prefix); + this.maxSize = config.getInt(prefix + "size", this.maxSize); + this.nBuckets = config.getInt(prefix + "buckets", this.nBuckets); + // In seconds. + this.durBucket = (long) (config.getDouble(prefix + "time", (float) this.durBucket / 1000f) * 1000f); + this.factor = (float) config.getDouble(prefix + "factor", this.factor); + return this; + } + } + + protected final int maxSize; + protected final LinkedHashMap entries; + protected final long durBucket; + protected final int nBuckets; + protected final float factor; + + protected long lastAdd = System.currentTimeMillis(); + + public FlatWords(String name, FlatWordsSettings settings){ + this(name, settings.maxSize, settings.nBuckets, settings.durBucket, settings.factor, settings.sort, settings.compress, settings.split); + this.weight = settings.weight; + } + + public FlatWords(String name, int maxSize, int nBuckets, long durBucket, float factor, boolean sort, boolean compress, boolean split){ + super(name, sort, compress, split); + this.maxSize = maxSize; + entries = new LinkedHashMap(maxSize); + this.nBuckets = nBuckets; + this.durBucket = durBucket; + this.factor = factor; + } + + @Override + public void start(final MessageLetterCount message) { + if (System.currentTimeMillis() - lastAdd > nBuckets * durBucket) + entries.clear(); + else if (entries.size() + message.words.length > maxSize) + releaseMap(entries, Math.max(message.words.length, maxSize / 10)); + } + + @Override + public void clear() { + super.clear(); + entries.clear(); + } + + @Override + protected float getScore(List chars, long ts) { + lastAdd = ts; + final char[] a = DigestedWords.toArray(chars); + final String key = new String(a); + ActionFrequency freq = entries.get(key); + if (freq == null){ + freq = new ActionFrequency(nBuckets, durBucket); + entries.put(key, freq); + return 0.0f; + } + freq.update(ts); + float score = Math.min(1.0f, freq.getScore(factor)); + freq.add(ts, 1.0f); + return score; + } + +} diff --git a/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/processors/SimilarWordsBKL.java b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/processors/SimilarWordsBKL.java new file mode 100644 index 00000000..75b42571 --- /dev/null +++ b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/processors/SimilarWordsBKL.java @@ -0,0 +1,102 @@ +package fr.neatmonster.nocheatplus.checks.chat.analysis.engine.processors; + +import java.util.List; + +import fr.neatmonster.nocheatplus.checks.chat.analysis.MessageLetterCount; +import fr.neatmonster.nocheatplus.checks.chat.analysis.ds.bktree.SimpleTimedBKLevenshtein; +import fr.neatmonster.nocheatplus.checks.chat.analysis.ds.bktree.SimpleTimedBKLevenshtein.STBKLResult; +import fr.neatmonster.nocheatplus.checks.chat.analysis.ds.bktree.TimedBKLevenshtein.SimpleTimedLevenNode; +import fr.neatmonster.nocheatplus.config.ConfigFile; + +public class SimilarWordsBKL extends DigestedWords { + + public static class SimilarWordsBKLSettings extends DigestedWordsSettings{ + public int maxSize = 1000; + public int range = 2; + public long durExpire = 30000; + public int maxSeek = 0; + /** + * split + compress by default. + */ + public SimilarWordsBKLSettings(){ + super(false, true, true, 1f); + } + public SimilarWordsBKLSettings(int range, long durExpire, int maxSize, int maxSeek, boolean sort, boolean compress, boolean split, float weight) { + super(sort, compress, split, weight); + this.range = range; + this.durExpire = durExpire; + this.maxSize = maxSize; + this.maxSeek = maxSeek; + } + public SimilarWordsBKLSettings applyConfig(ConfigFile config, String prefix){ + super.applyConfig(config, prefix); + this.maxSize = config.getInt(prefix + "size", this.maxSize); + this.maxSeek= config.getInt(prefix + "seek", this.maxSeek); + this.durExpire = (long) (config.getDouble(prefix + "time", (float) this.durExpire / 1000f) * 1000f); + return this; + } + } + + protected final SimpleTimedBKLevenshtein tree = new SimpleTimedBKLevenshtein(); + + protected int added = 0; + protected final int maxSize; + + protected final int range; + + protected final long durExpire; + + protected final int maxSeek; + + protected long lastAdd = System.currentTimeMillis(); + + public SimilarWordsBKL(String name, SimilarWordsBKLSettings settings){ + this(name, settings.range, settings.durExpire, settings.maxSize, settings.maxSeek , settings.sort, settings.compress, settings.split); + this.weight = settings.weight; + } + + public SimilarWordsBKL(String name, int range, long durExpire, int maxSize, int maxSeek, boolean sort, boolean compress, boolean split) { + super(name, sort, compress, split); + this.maxSize = maxSize; + this.range = range; + this.durExpire = durExpire; + this.maxSeek = maxSeek; + } + + @Override + public void clear() { + super.clear(); + tree.clear(); + } + + @Override + public void start(final MessageLetterCount message) { + if (added + message.words.length > maxSize || System.currentTimeMillis() - lastAdd > durExpire) tree.clear(); + + } + + @Override + protected float getScore(final List chars, final long ts) { + // TODO: very short words, very long words. + lastAdd = ts; + final char[] a = DigestedWords.toArray(chars); + final STBKLResult result = tree.lookup(a, range, maxSeek, true); + if (result.isNew) added ++; + // Calculate time score. + float score = 0f; + if (!result.isNew && result.match != null){ + final long age = ts - result.match.ts; + result.match.ts = ts; + if (age < durExpire) + score = Math.max(score, (float) (durExpire - age) / (float) durExpire); + } + for (final SimpleTimedLevenNode node : result.nodes){ + final long age = ts - node.ts; + node.ts = ts; + if (age < durExpire) + score = Math.max(score, (float) (durExpire - age) / (float) durExpire); + } + return score; + } + +} diff --git a/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/processors/WordPrefixes.java b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/processors/WordPrefixes.java new file mode 100644 index 00000000..d7b50f9b --- /dev/null +++ b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/processors/WordPrefixes.java @@ -0,0 +1,99 @@ +package fr.neatmonster.nocheatplus.checks.chat.analysis.engine.processors; + +import java.util.List; + +import fr.neatmonster.nocheatplus.checks.chat.analysis.MessageLetterCount; +import fr.neatmonster.nocheatplus.checks.chat.analysis.ds.prefixtree.SimpleTimedCharPrefixTree; +import fr.neatmonster.nocheatplus.checks.chat.analysis.ds.prefixtree.SimpleTimedCharPrefixTree.SimpleTimedCharLookupEntry; +import fr.neatmonster.nocheatplus.config.ConfigFile; + +public class WordPrefixes extends DigestedWords{ + + public static class WordPrefixesSettings extends DigestedWordsSettings{ + public int maxAdd = 1000; + public long durExpire = 30000; + /** + * split and compress by default. + */ + public WordPrefixesSettings(){ + super(false, true, true, 1f); + } + public WordPrefixesSettings(long durExpire, int maxAdd, boolean sort, boolean compress, boolean split, float weight) { + super(sort, compress, split, weight); + this.maxAdd = maxAdd; + this.durExpire = durExpire; + } + public WordPrefixesSettings applyConfig(ConfigFile config, String prefix){ + super.applyConfig(config, prefix); + this.maxAdd = config.getInt(prefix + "size", this.maxAdd); + this.durExpire = (long) (config.getDouble(prefix + "time", (float) this.durExpire / 1000f) * 1000f); + return this; + } + } + + protected final SimpleTimedCharPrefixTree tree = new SimpleTimedCharPrefixTree(true); + + protected final int maxAdd; + + protected int added = 0; + + protected final long durExpire; + + protected long lastAdd = System.currentTimeMillis(); + + public WordPrefixes(String name, WordPrefixesSettings settings){ + this(name, settings.durExpire, settings.maxAdd, settings.sort, settings.compress, settings.split); + this.weight = settings.weight; + } + + /** + * + * @param durExpire + * @param maxAdd + * @param sort Sort letters. + * @param compress Only use every letter once. + * @param split Check for letters, digits, other individually (!). + */ + public WordPrefixes(String name, long durExpire, int maxAdd, boolean sort, boolean compress, boolean split) { + super(name, sort, compress, split); + this.durExpire = durExpire; + this.maxAdd = maxAdd; + } + + @Override + public void start(final MessageLetterCount message) { + // This allows adding up to maximum messge length more characters, + // but also allows to set size of nodes exactly. + // TODO: Some better method than blunt clear (extra LinkedHashSet/LRU?). + if (added > maxAdd || System.currentTimeMillis() - lastAdd > durExpire){ + tree.clear(); + added = 0; + } + } + + @Override + public void clear() { + super.clear(); + tree.clear(); + } + + protected float getScore(final List chars, final long ts) { + lastAdd = ts; + final int len = chars.size(); + final SimpleTimedCharLookupEntry entry = tree.lookup(chars, true); + final int depth = entry.depth; + float score = 0f; + for (int i = 0; i < depth ; i++){ + final long age = ts - entry.timeInsertion[i]; + if (age < durExpire) + score += 1f / (float) (depth - i) * (float) (durExpire - age) / (float) durExpire; + } + if (depth == len){ + score += 0.2; + if (entry.insertion.isEnd) score += 0.2; + } + if (len != depth) added += len - depth; + return score; + } + +} diff --git a/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/WordProcessor.java b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/processors/WordProcessor.java similarity index 70% rename from src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/WordProcessor.java rename to src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/processors/WordProcessor.java index def7dbc6..d928c56f 100644 --- a/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/WordProcessor.java +++ b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/engine/processors/WordProcessor.java @@ -1,4 +1,4 @@ -package fr.neatmonster.nocheatplus.checks.chat.analysis.engine; +package fr.neatmonster.nocheatplus.checks.chat.analysis.engine.processors; import fr.neatmonster.nocheatplus.checks.chat.analysis.MessageLetterCount; @@ -11,6 +11,12 @@ public interface WordProcessor{ */ public String getProcessorName(); + /** + * Configured weight. + * @return + */ + public float getWeight(); + /** * * @param message diff --git a/src/fr/neatmonster/nocheatplus/config/ConfPaths.java b/src/fr/neatmonster/nocheatplus/config/ConfPaths.java index 7e5f495c..88f02134 100644 --- a/src/fr/neatmonster/nocheatplus/config/ConfPaths.java +++ b/src/fr/neatmonster/nocheatplus/config/ConfPaths.java @@ -141,25 +141,39 @@ public abstract class ConfPaths { public static final String CHAT_COLOR_CHECK = CHAT_COLOR + "active"; public static final String CHAT_COLOR_ACTIONS = CHAT_COLOR + "actions"; + // globalchat private static final String CHAT_GLOBALCHAT = CHAT + "globalchat."; public static final String CHAT_GLOBALCHAT_CHECK = CHAT_GLOBALCHAT + "active"; - private static final String CHAT_GLOBALCHAT_ENGINE = CHAT_GLOBALCHAT + "engine."; - public static final String CHAT_GLOBALCHAT_ENGINE_CHECK = CHAT_GLOBALCHAT_ENGINE + "active"; - private static final String CHAT_GLOBALCHAT_ENGINE_GLWORDFREQ = CHAT_GLOBALCHAT_ENGINE + "glwordfrequency."; - public static final String CHAT_GLOBALCHAT_ENGINE_GLWORDFREQ_CHECK = CHAT_GLOBALCHAT_ENGINE_GLWORDFREQ + "active"; - public static final String CHAT_GLOBALCHAT_ENGINE_GLCOMPRWORDS = CHAT_GLOBALCHAT_ENGINE + "glcompressedwords."; - public static final String CHAT_GLOBALCHAT_ENGINE_GLCOMPRWORDS_CHECK = CHAT_GLOBALCHAT_ENGINE_GLCOMPRWORDS + "active"; - private static final String CHAT_GLOBALCHAT_ENGINE_PPCOMPRWORDS = CHAT_GLOBALCHAT_ENGINE + "ppcompressedwords."; - public static final String CHAT_GLOBALCHAT_ENGINE_PPCOMPRWORDS_CHECK = CHAT_GLOBALCHAT_ENGINE_PPCOMPRWORDS + "check"; - private static final String CHAT_GLOBALCHAT_ENGINE_PPWORDFREQ = CHAT_GLOBALCHAT_ENGINE + "ppwordfrequency."; - public static final String CHAT_GLOBALCHAT_ENGINE_PPWORDFREQ_CHECK = CHAT_GLOBALCHAT_ENGINE_PPWORDFREQ + "active"; - public static final String CHAT_GLOBALCHAT_COMMANDS = CHAT_GLOBALCHAT + "commands"; + public static final String CHAT_GLOBALCHAT_LEVEL = CHAT_GLOBALCHAT + "level"; + public static final String CHAT_GLOBALCHAT_ENGINE_MAXIMUM = CHAT_GLOBALCHAT + "maximum"; public static final String CHAT_GLOBALCHAT_FREQUENCY = CHAT_GLOBALCHAT + "frequency."; public static final String CHAT_GLOBALCHAT_FREQUENCY_WEIGHT = CHAT_GLOBALCHAT_FREQUENCY + "weight"; public static final String CHAT_GLOBALCHAT_FREQUENCY_FACTOR = CHAT_GLOBALCHAT_FREQUENCY + "factor"; - public static final String CHAT_GLOBALCHAT_LEVEL = CHAT_GLOBALCHAT + "level"; + public static final String CHAT_GLOBALCHAT_COMMANDS = CHAT_GLOBALCHAT + "commands"; + // (Some of the following paths must be public for generic config reading.) + // Extended global checks. + private static final String CHAT_GLOBALCHAT_GL = CHAT_GLOBALCHAT + "global."; + public static final String CHAT_GLOBALCHAT_GL_WEIGHT = CHAT_GLOBALCHAT_GL + "weight"; + public static final String CHAT_GLOBALCHAT_GL_WORDS = CHAT_GLOBALCHAT_GL + "words."; + public static final String CHAT_GLOBALCHAT_GL_WORDS_CHECK = CHAT_GLOBALCHAT_GL_WORDS + "active"; + public static final String CHAT_GLOBALCHAT_GL_PREFIXES = CHAT_GLOBALCHAT_GL + "prefixes."; + public static final String CHAT_GLOBALCHAT_GL_PREFIXES_CHECK = CHAT_GLOBALCHAT_GL_PREFIXES + "active"; + public static final String CHAT_GLOBALCHAT_GL_SIMILARITY = CHAT_GLOBALCHAT_GL + "similarity."; + public static final String CHAT_GLOBALCHAT_GL_SIMILARITY_CHECK = CHAT_GLOBALCHAT_GL_SIMILARITY + "active"; + // Extended per player checks. + private static final String CHAT_GLOBALCHAT_PP = CHAT_GLOBALCHAT + "player."; + public static final String CHAT_GLOBALCHAT_PP_WEIGHT = CHAT_GLOBALCHAT_PP + "weight"; + public static final String CHAT_GLOBALCHAT_PP_PREFIXES = CHAT_GLOBALCHAT_PP + "prefixes."; + public static final String CHAT_GLOBALCHAT_PP_PREFIXES_CHECK = CHAT_GLOBALCHAT_PP_PREFIXES + "active"; + public static final String CHAT_GLOBALCHAT_PP_WORDS = CHAT_GLOBALCHAT_PP + "words."; + public static final String CHAT_GLOBALCHAT_PP_WORDS_CHECK = CHAT_GLOBALCHAT_PP_WORDS + "active"; + public static final String CHAT_GLOBALCHAT_PP_SIMILARITY = CHAT_GLOBALCHAT_PP + "similarity."; + public static final String CHAT_GLOBALCHAT_PP_SIMILARITY_CHECK = CHAT_GLOBALCHAT_PP_SIMILARITY + "active"; + // globalchat actions + public static final String CHAT_GLOBALCHAT_DEBUG = CHAT_GLOBALCHAT + "debug"; public static final String CHAT_GLOBALCHAT_ACTIONS = CHAT_GLOBALCHAT + "actions"; - + + // nopwnage private static final String CHAT_NOPWNAGE = CHAT + "nopwnage."; public static final String CHAT_NOPWNAGE_CHECK = CHAT_NOPWNAGE + "active"; public static final String CHAT_NOPWNAGE_EXCLUSIONS = CHAT_NOPWNAGE + "exclusions"; diff --git a/src/fr/neatmonster/nocheatplus/config/DefaultConfig.java b/src/fr/neatmonster/nocheatplus/config/DefaultConfig.java index 82a41e86..f2994ab1 100644 --- a/src/fr/neatmonster/nocheatplus/config/DefaultConfig.java +++ b/src/fr/neatmonster/nocheatplus/config/DefaultConfig.java @@ -128,21 +128,24 @@ public class DefaultConfig extends ConfigFile { set(ConfPaths.CHAT_COLOR_CHECK, true); set(ConfPaths.CHAT_COLOR_ACTIONS, "log:color:0:1:if cancel"); + // globalchat (ordering on purpose). set(ConfPaths.CHAT_GLOBALCHAT_CHECK, true); - set(ConfPaths.CHAT_GLOBALCHAT_COMMANDS, new LinkedList(Arrays.asList( - new String[]{"/me"}))); - set(ConfPaths.CHAT_GLOBALCHAT_ENGINE_CHECK, false); - // Individual engine settings: maybe hide later by checking another "expert" or "show-hidden" flag. - set(ConfPaths.CHAT_GLOBALCHAT_ENGINE_GLWORDFREQ_CHECK, false); - set(ConfPaths.CHAT_GLOBALCHAT_ENGINE_GLCOMPRWORDS_CHECK, false); - set(ConfPaths.CHAT_GLOBALCHAT_ENGINE_PPWORDFREQ_CHECK, false); - set(ConfPaths.CHAT_GLOBALCHAT_ENGINE_PPCOMPRWORDS_CHECK, true); - // + set(ConfPaths.CHAT_GLOBALCHAT_LEVEL, 80); set(ConfPaths.CHAT_GLOBALCHAT_FREQUENCY_FACTOR, 0.9D); set(ConfPaths.CHAT_GLOBALCHAT_FREQUENCY_WEIGHT, 6); - set(ConfPaths.CHAT_GLOBALCHAT_LEVEL, 80); + set(ConfPaths.CHAT_GLOBALCHAT_COMMANDS, + new LinkedList(Arrays.asList(new String[]{"/me"}))); + set(ConfPaths.CHAT_GLOBALCHAT_GL_WEIGHT, 0.5); + set(ConfPaths.CHAT_GLOBALCHAT_GL_WORDS_CHECK, false); + set(ConfPaths.CHAT_GLOBALCHAT_GL_PREFIXES_CHECK , false); + set(ConfPaths.CHAT_GLOBALCHAT_GL_SIMILARITY_CHECK , false); + set(ConfPaths.CHAT_GLOBALCHAT_PP_WORDS_CHECK, false); + set(ConfPaths.CHAT_GLOBALCHAT_PP_PREFIXES_CHECK, false); + set(ConfPaths.CHAT_GLOBALCHAT_PP_SIMILARITY_CHECK , false); + // set(ConfPaths.CHAT_GLOBALCHAT_ACTIONS, "log:globalchat:0:5:f cancel cmd:tellglchat vl>20 log:globalchat:0:5:if cancel cmd:kickglchat"); - + + // nopwnage set(ConfPaths.CHAT_NOPWNAGE_CHECK, true); set(ConfPaths.CHAT_NOPWNAGE_EXCLUSIONS, new ArrayList()); set(ConfPaths.CHAT_NOPWNAGE_LEVEL, 500); @@ -153,7 +156,7 @@ public class DefaultConfig extends ConfigFile { set(ConfPaths.CHAT_NOPWNAGE_CAPTCHA_CHECK, true); set(ConfPaths.CHAT_NOPWNAGE_CAPTCHA_CHARACTERS, - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"); + "abcdefghjkmnpqrtuvwxyzABCDEFGHJKMNPQRTUVWXYZ2346789"); set(ConfPaths.CHAT_NOPWNAGE_CAPTCHA_LENGTH, 6); set(ConfPaths.CHAT_NOPWNAGE_CAPTCHA_QUESTION, "&cPlease type '&6[captcha]&c' to continue sending messages/commands."); diff --git a/src/fr/neatmonster/nocheatplus/utilities/CheckUtils.java b/src/fr/neatmonster/nocheatplus/utilities/CheckUtils.java index 43a6e3e0..2aa3b38b 100644 --- a/src/fr/neatmonster/nocheatplus/utilities/CheckUtils.java +++ b/src/fr/neatmonster/nocheatplus/utilities/CheckUtils.java @@ -1,7 +1,12 @@ package fr.neatmonster.nocheatplus.utilities; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.Collection; +import java.util.List; import java.util.logging.Logger; +import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Location; import org.bukkit.entity.Player; @@ -14,6 +19,17 @@ public class CheckUtils { /** The file logger. */ public static Logger fileLogger = null; + + /** Decimal format for "#.###" */ + public static final DecimalFormat fdec3 = new DecimalFormat(); + + static{ + DecimalFormatSymbols sym = fdec3.getDecimalFormatSymbols(); + sym.setDecimalSeparator('.'); + fdec3.setDecimalFormatSymbols(sym); + fdec3.setMaximumFractionDigits(3); + fdec3.setMinimumIntegerDigits(1); + } /** * Check if a player looks at a target of a specific size, with a specific precision value (roughly). @@ -158,6 +174,53 @@ public class CheckUtils { return p[n]; } + + /** + * Join parts with link. + * @param input + * @param link + * @return + */ + public static String join(final Collection input, final String link){ + final StringBuilder builder = new StringBuilder(Math.max(300, input.size() * 10)); + boolean first = true; + for (final Object obj : input){ + if (!first) builder.append(link); + builder.append(obj.toString()); + first = false; + } + return builder.toString(); + } + + /** + * Convenience method. + * @param parts + * @param link + * @return + */ + public static boolean scheduleOutputJoined(final List parts, String link){ + return scheduleOutput(join(parts, link)); + } + + /** + * Schedule a message to be output by the bukkit logger. + * @param message + * @return If scheduled successfully. + */ + public static boolean scheduleOutput(final String message){ + try{ + return Bukkit.getScheduler().scheduleSyncDelayedTask(Bukkit.getPluginManager().getPlugin("NoCheatPlus"), + new Runnable() { + @Override + public void run() { + Bukkit.getLogger().info(message); + } + }) != -1; + } + catch (final Exception exc){ + return false; + } + } /** * Removes the colors of a message.