diff --git a/src/fr/neatmonster/nocheatplus/actions/types/ActionList.java b/src/fr/neatmonster/nocheatplus/actions/types/ActionList.java index 92157210..049d5373 100644 --- a/src/fr/neatmonster/nocheatplus/actions/types/ActionList.java +++ b/src/fr/neatmonster/nocheatplus/actions/types/ActionList.java @@ -26,7 +26,7 @@ public class ActionList { /** This is a very bad design decision, but it's also really convenient to define this here. */ public final String permissionSilent; - /** If there are no actions registered, we still return an Array. It's just empty/size=0. */ + /** If there are no actions registered, we still return an Array. It's just empty/maxSize=0. */ private final static Action[] emptyArray = new Action[0]; /** The actions of this ActionList, "bundled" by treshold (violation level). */ diff --git a/src/fr/neatmonster/nocheatplus/checks/chat/GlobalChat.java b/src/fr/neatmonster/nocheatplus/checks/chat/GlobalChat.java index 1c1f2d6c..5695fd3d 100644 --- a/src/fr/neatmonster/nocheatplus/checks/chat/GlobalChat.java +++ b/src/fr/neatmonster/nocheatplus/checks/chat/GlobalChat.java @@ -4,7 +4,9 @@ import org.bukkit.entity.Player; import fr.neatmonster.nocheatplus.checks.Check; import fr.neatmonster.nocheatplus.checks.CheckType; -import fr.neatmonster.nocheatplus.checks.chat.MessageLetterCount.WordLetterCount; +import fr.neatmonster.nocheatplus.checks.chat.analysis.LetterEngine; +import fr.neatmonster.nocheatplus.checks.chat.analysis.MessageLetterCount; +import fr.neatmonster.nocheatplus.checks.chat.analysis.WordLetterCount; import fr.neatmonster.nocheatplus.hooks.NCPExemptionManager; /** @@ -14,6 +16,8 @@ import fr.neatmonster.nocheatplus.hooks.NCPExemptionManager; */ public class GlobalChat extends Check{ + private final LetterEngine engine = new LetterEngine(); + public GlobalChat() { super(CheckType.CHAT_GLOBALCHAT); } @@ -79,6 +83,8 @@ public class GlobalChat extends Check{ // (Following: random/made up criteria.) + // TODO: Create tests for all methods with wordlists, fake chat (refactor for that). + // Full message processing. ------------ // Upper case. @@ -128,7 +134,9 @@ public class GlobalChat extends Check{ wWords /= (float) letterCounts.words.length; score += wWords; - // TODO: LetterEngine ? + core checks + // Engine: + final float wEngine = engine.feed(letterCounts); + score += wEngine; // Wrapping it up. -------------------- // Add weight to frequency counts. diff --git a/src/fr/neatmonster/nocheatplus/checks/chat/MessageLetterCount.java b/src/fr/neatmonster/nocheatplus/checks/chat/MessageLetterCount.java deleted file mode 100644 index 50117472..00000000 --- a/src/fr/neatmonster/nocheatplus/checks/chat/MessageLetterCount.java +++ /dev/null @@ -1,104 +0,0 @@ -package fr.neatmonster.nocheatplus.checks.chat; - -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; - -/** - * Letter count for a message and sub words, including upper case count.
- * NOTE: this is a pretty heavy implementation for testing purposes. - * @author mc_dev - * - */ -public class MessageLetterCount { - /** - * Letter count for a word. - * @author mc_dev - * - */ - public static final class WordLetterCount{ - public final String word; - public final Map counts; - public final int upperCase; - public final int notLetter; - public WordLetterCount(final String word){ - this.word = word; - char[] a = word.toCharArray(); - // Preserve insertion order. - counts = new LinkedHashMap(a.length); - int upperCase = 0; - int notLetter = 0; - for (int i = 0; i < a.length; i++){ - final char c = a[i]; - final Character key; - if (!Character.isLetter(c)) notLetter ++; - if (Character.isUpperCase(c)){ - upperCase ++; - key = Character.toLowerCase(c); - } - else key = c; - final Integer count = counts.remove(key); - if (count == null) counts.put(key, 1); - else counts.put(key, count.intValue() + 1); - - } - this.notLetter = notLetter; - this.upperCase = upperCase; - } - - public float getNotLetterRatio(){ - return (float) notLetter / (float) word.length(); - } - - public float getLetterCountRatio(){ - return (float) counts.size() / (float) word.length(); - } - - public float getUpperCaseRatio(){ - return (float) upperCase / (float) word.length(); - } - } - - public final String message; - - public final String split; - - public final WordLetterCount[] words; - - public final WordLetterCount fullCount; - - /** - * Constructor for splitting by a space. - * @param message - */ - public MessageLetterCount(final String message){ - this(message, " "); - } - - /** - * - * @param message - * @param split - */ - public MessageLetterCount(final String message, final String split){ - this.message = message; - this.split = split; - - final String[] parts = message.split(split); - words = new WordLetterCount[parts.length]; - - fullCount = new WordLetterCount(message); - // (Do not store 60 times "a".) - final Map done = new HashMap(words.length); - for (int i = 0; i < parts.length; i++){ - final String word = parts[i]; - if (done.containsKey(word)){ - words[i] = done.get(word); - continue; - } - done.put(word, words[i] = new WordLetterCount(word)); - } - done.clear(); - } - -} diff --git a/src/fr/neatmonster/nocheatplus/checks/chat/analysis/AbstractWordProcessor.java b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/AbstractWordProcessor.java new file mode 100644 index 00000000..2860d8d2 --- /dev/null +++ b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/AbstractWordProcessor.java @@ -0,0 +1,64 @@ +package fr.neatmonster.nocheatplus.checks.chat.analysis; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +public abstract class AbstractWordProcessor implements WordProcessor{ + + /** + * Remove a number of entries from a map, in order of iteration over a key set. + * @param map + * @param number Number of entries to remove. + */ + public static final void releaseMap(final Map map, int number){ + final List rem = new LinkedList(); + int i = 0; + for (final K key : map.keySet()){ + rem.add(key); + i++; + if (i > number) break; + } + for (final K key : rem){ + map.remove(key); + } + } + + protected String name; + public AbstractWordProcessor(String name){ + this.name = name; + } + + @Override + public String getProcessorName(){ + return name; + } + + @Override + public float process(final MessageLetterCount message) { + // Does the looping, scores are summed up and divided by number of words. + start(message); + final long ts = System.currentTimeMillis(); + float score = 0; + 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 /= (float) message.words.length; +// System.out.println(getProcessorName() +": " + score); + return score; + } + + public void start(final MessageLetterCount message){ + // Override if needed. + } + + /** + * Process one word. + * @param index + * @param message + * @return Score. + */ + 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/FlatWordBuckets.java b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/FlatWordBuckets.java new file mode 100644 index 00000000..f9f15ff8 --- /dev/null +++ b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/FlatWordBuckets.java @@ -0,0 +1,47 @@ +package fr.neatmonster.nocheatplus.checks.chat.analysis; + +import java.util.LinkedHashMap; + +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(MessageLetterCount message) { + if (entries.size() + message.words.length > maxSize) + releaseMap(entries, maxSize / 10); + } + + @Override + public float loop(long ts, int index, String key, + WordLetterCount message) { + 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/LetterEngine.java b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/LetterEngine.java new file mode 100644 index 00000000..4f25ad43 --- /dev/null +++ b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/LetterEngine.java @@ -0,0 +1,31 @@ +package fr.neatmonster.nocheatplus.checks.chat.analysis; + +import java.util.ArrayList; +import java.util.List; + + +/** + * Process words. + * @author mc_dev + * + */ +public class LetterEngine { + + protected final List processors = new ArrayList(); + + public LetterEngine(){ + // Add word processors. + processors.add(new FlatWordBuckets(1000, 4, 1500, 0.9f)); + } + + public float feed(final MessageLetterCount letterCount){ + float score = 0; + // Run all processors. + for (final WordProcessor processor : processors){ + final float refScore = processor.process(letterCount); + // TODO: max or sum or average or flexible (?)... + score = Math.max(score, refScore); + } + return score; + } +} diff --git a/src/fr/neatmonster/nocheatplus/checks/chat/analysis/MessageLetterCount.java b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/MessageLetterCount.java new file mode 100644 index 00000000..5391ec3c --- /dev/null +++ b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/MessageLetterCount.java @@ -0,0 +1,56 @@ +package fr.neatmonster.nocheatplus.checks.chat.analysis; + +import java.util.HashMap; +import java.util.Map; + +/** + * Letter count for a message and sub words, including upper case count.
+ * NOTE: this is a pretty heavy implementation for testing purposes. + * @author mc_dev + * + */ +public class MessageLetterCount { + + public final String message; + + public final String split; + + public final WordLetterCount[] words; + + public final WordLetterCount fullCount; + + /** + * Constructor for splitting by a space. + * @param message + */ + public MessageLetterCount(final String message){ + this(message, " "); + } + + /** + * + * @param message + * @param split + */ + public MessageLetterCount(final String message, final String split){ + this.message = message; + this.split = split; + + final String[] parts = message.split(split); + words = new WordLetterCount[parts.length]; + + fullCount = new WordLetterCount(message); + // (Do not store 60 times "a".) + final Map done = new HashMap(words.length); + for (int i = 0; i < parts.length; i++){ + final String word = parts[i]; + if (done.containsKey(word)){ + words[i] = done.get(word); + continue; + } + done.put(word, words[i] = new WordLetterCount(word)); + } + done.clear(); + } + +} diff --git a/src/fr/neatmonster/nocheatplus/checks/chat/analysis/WordLetterCount.java b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/WordLetterCount.java new file mode 100644 index 00000000..5fc619a8 --- /dev/null +++ b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/WordLetterCount.java @@ -0,0 +1,52 @@ +package fr.neatmonster.nocheatplus.checks.chat.analysis; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Letter count for a word. + * @author mc_dev + * + */ +public final class WordLetterCount{ + public final String word; + public final Map counts; + public final int upperCase; + public final int notLetter; + public WordLetterCount(final String word){ + this.word = word; + char[] a = word.toCharArray(); + // Preserve insertion order. + counts = new LinkedHashMap(a.length); + int upperCase = 0; + int notLetter = 0; + for (int i = 0; i < a.length; i++){ + final char c = a[i]; + final Character key; + if (!Character.isLetter(c)) notLetter ++; + if (Character.isUpperCase(c)){ + upperCase ++; + key = Character.toLowerCase(c); + } + else key = c; + final Integer count = counts.remove(key); + if (count == null) counts.put(key, 1); + else counts.put(key, count.intValue() + 1); + + } + this.notLetter = notLetter; + this.upperCase = upperCase; + } + + public float getNotLetterRatio(){ + return (float) notLetter / (float) word.length(); + } + + public float getLetterCountRatio(){ + return (float) counts.size() / (float) word.length(); + } + + public float getUpperCaseRatio(){ + return (float) upperCase / (float) word.length(); + } +} diff --git a/src/fr/neatmonster/nocheatplus/checks/chat/analysis/WordProcessor.java b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/WordProcessor.java new file mode 100644 index 00000000..05bcbc41 --- /dev/null +++ b/src/fr/neatmonster/nocheatplus/checks/chat/analysis/WordProcessor.java @@ -0,0 +1,12 @@ +package fr.neatmonster.nocheatplus.checks.chat.analysis; + + +public interface WordProcessor{ + public String getProcessorName(); + /** + * + * @param message + * @return A number ranging from 0 to 1. 0 means no matching, 1 means high repetition score. + */ + public float process(MessageLetterCount message); +}