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);
+}