mirror of
https://github.com/NoCheatPlus/NoCheatPlus.git
synced 2025-01-07 16:28:13 +01:00
Re-structure globalchat: rename config paths, add hidden settings, add
similarity check. Adjust default captcha letters (Ticket 194).
This commit is contained in:
parent
89a5b3221f
commit
d4103899a5
@ -71,11 +71,14 @@ public class ChatConfig implements CheckConfig {
|
|||||||
public final ActionList colorActions;
|
public final ActionList colorActions;
|
||||||
|
|
||||||
public final boolean globalChatCheck;
|
public final boolean globalChatCheck;
|
||||||
public final boolean globalChatEngineCheck;
|
|
||||||
public final EnginePlayerConfig globalChatEnginePlayerConfig;
|
public final EnginePlayerConfig globalChatEnginePlayerConfig;
|
||||||
public final float globalChatFrequencyFactor;
|
public final float globalChatFrequencyFactor;
|
||||||
public final float globalChatFrequencyWeight;
|
public final float globalChatFrequencyWeight;
|
||||||
|
public final float globalChatGlobalWeight;
|
||||||
|
public final float globalChatPlayerWeight;
|
||||||
public final double globalChatLevel;
|
public final double globalChatLevel;
|
||||||
|
public boolean globalChatEngineMaximum;
|
||||||
|
public final boolean globalChatDebug;
|
||||||
public final ActionList globalChatActions;
|
public final ActionList globalChatActions;
|
||||||
|
|
||||||
public final boolean noPwnageCheck;
|
public final boolean noPwnageCheck;
|
||||||
@ -145,11 +148,14 @@ public class ChatConfig implements CheckConfig {
|
|||||||
colorActions = config.getActionList(ConfPaths.CHAT_COLOR_ACTIONS, Permissions.CHAT_COLOR);
|
colorActions = config.getActionList(ConfPaths.CHAT_COLOR_ACTIONS, Permissions.CHAT_COLOR);
|
||||||
|
|
||||||
globalChatCheck = config.getBoolean(ConfPaths.CHAT_GLOBALCHAT_CHECK);
|
globalChatCheck = config.getBoolean(ConfPaths.CHAT_GLOBALCHAT_CHECK);
|
||||||
globalChatEngineCheck = config.getBoolean(ConfPaths.CHAT_GLOBALCHAT_ENGINE_CHECK);
|
|
||||||
globalChatEnginePlayerConfig = new EnginePlayerConfig(config);
|
globalChatEnginePlayerConfig = new EnginePlayerConfig(config);
|
||||||
globalChatFrequencyFactor = (float) config.getDouble(ConfPaths.CHAT_GLOBALCHAT_FREQUENCY_FACTOR);
|
globalChatFrequencyFactor = (float) config.getDouble(ConfPaths.CHAT_GLOBALCHAT_FREQUENCY_FACTOR);
|
||||||
globalChatFrequencyWeight = (float) config.getDouble(ConfPaths.CHAT_GLOBALCHAT_FREQUENCY_WEIGHT);
|
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);
|
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);
|
globalChatActions = config.getActionList(ConfPaths.CHAT_GLOBALCHAT_ACTIONS, Permissions.CHAT_GLOBALCHAT);
|
||||||
|
|
||||||
noPwnageCheck = config.getBoolean(ConfPaths.CHAT_NOPWNAGE_CHECK);
|
noPwnageCheck = config.getBoolean(ConfPaths.CHAT_NOPWNAGE_CHECK);
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
package fr.neatmonster.nocheatplus.checks.chat;
|
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 org.bukkit.entity.Player;
|
||||||
|
|
||||||
import fr.neatmonster.nocheatplus.checks.Check;
|
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.ConfigFile;
|
||||||
import fr.neatmonster.nocheatplus.config.ConfigManager;
|
import fr.neatmonster.nocheatplus.config.ConfigManager;
|
||||||
import fr.neatmonster.nocheatplus.hooks.NCPExemptionManager;
|
import fr.neatmonster.nocheatplus.hooks.NCPExemptionManager;
|
||||||
|
import fr.neatmonster.nocheatplus.utilities.CheckUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Some alternative more or less advanced analysis methods.
|
* Some alternative more or less advanced analysis methods.
|
||||||
@ -48,7 +54,7 @@ public class GlobalChat extends Check implements INotifyReload{
|
|||||||
|
|
||||||
synchronized (data) {
|
synchronized (data) {
|
||||||
return unsafeCheck(player, message, captcha, cc, data, isMainThread);
|
return unsafeCheck(player, message, captcha, cc, data, isMainThread);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void init() {
|
private void init() {
|
||||||
@ -89,6 +95,15 @@ public class GlobalChat extends Check implements INotifyReload{
|
|||||||
|
|
||||||
boolean cancel = false;
|
boolean cancel = false;
|
||||||
|
|
||||||
|
boolean debug = cc.globalChatDebug;
|
||||||
|
|
||||||
|
final List<String> debugParts;
|
||||||
|
if (debug){
|
||||||
|
debugParts = new LinkedList<String>();
|
||||||
|
debugParts.add("[NoCheatPlus][globalchat] Message ("+player.getName()+"/"+message.length()+"): ");
|
||||||
|
}
|
||||||
|
else debugParts = null;
|
||||||
|
|
||||||
// Update the frequency interval weights.
|
// Update the frequency interval weights.
|
||||||
data.globalChatFrequency.update(time);
|
data.globalChatFrequency.update(time);
|
||||||
|
|
||||||
@ -152,22 +167,28 @@ public class GlobalChat extends Check implements INotifyReload{
|
|||||||
wWords /= (float) letterCounts.words.length;
|
wWords /= (float) letterCounts.words.length;
|
||||||
score += wWords;
|
score += wWords;
|
||||||
|
|
||||||
|
if (debug && score > 0f) debugParts.add("Simple score: " + CheckUtils.fdec3.format(score));
|
||||||
|
|
||||||
// Engine:
|
// Engine:
|
||||||
if (cc.globalChatEngineCheck){
|
// TODO: more fine grained sync !
|
||||||
final float wEngine;
|
float wEngine = 0f;
|
||||||
synchronized (engine) {
|
final Map<String, Float> engMap;
|
||||||
wEngine = engine.process(letterCounts, player.getName(), cc, data);
|
synchronized (engine) {
|
||||||
}
|
engMap = engine.process(letterCounts, player.getName(), cc, data);
|
||||||
score += wEngine;
|
// 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. --------------------
|
// Wrapping it up. --------------------
|
||||||
// Add weight to frequency counts.
|
// Add weight to frequency counts.
|
||||||
data.globalChatFrequency.add(time, score);
|
data.globalChatFrequency.add(time, score);
|
||||||
final float accumulated = cc.globalChatFrequencyWeight * data.globalChatFrequency.getScore(cc.globalChatFrequencyFactor);
|
final float accumulated = cc.globalChatFrequencyWeight * data.globalChatFrequency.getScore(cc.globalChatFrequencyFactor);
|
||||||
|
|
||||||
// System.out.println("Total score: " + score + " (" + accumulated + ")");
|
|
||||||
|
|
||||||
if (score < 2.0f * cc.globalChatFrequencyWeight)
|
if (score < 2.0f * cc.globalChatFrequencyWeight)
|
||||||
// Reset the VL.
|
// Reset the VL.
|
||||||
data.globalChatVL = 0.0;
|
data.globalChatVL = 0.0;
|
||||||
@ -185,7 +206,21 @@ public class GlobalChat extends Check implements INotifyReload{
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
data.globalChatVL *= 0.95;
|
data.globalChatVL *= 0.95;
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
final List<String> keys = new LinkedList<String>(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;
|
return cancel;
|
||||||
}
|
}
|
||||||
|
@ -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 <N>
|
||||||
|
*/
|
||||||
|
public class BKLevenshtein<N extends LevenNode<N>, L extends LookupEntry<char[], N>> extends BKModTree<char[], N, L> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fat default impl.
|
||||||
|
* @author mc_dev
|
||||||
|
*
|
||||||
|
* @param <N>
|
||||||
|
*/
|
||||||
|
public static class LevenNode<N extends LevenNode<N>> extends HashMapNode<char[], N>{
|
||||||
|
public LevenNode(char[] value) {
|
||||||
|
super(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public BKLevenshtein(NodeFactory<char[], N> nodeFactory, LookupEntryFactory<char[], N, L> 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];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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<V, N extends Node<V, N>, L extends LookupEntry<V, N>>{
|
||||||
|
|
||||||
|
// 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 <V>
|
||||||
|
* @param <N>
|
||||||
|
*/
|
||||||
|
public static abstract class Node<V, N extends Node<V, N>>{
|
||||||
|
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<N> getChildren(final int distance, final int range, final Collection<N> nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node using a map as base, with basic implementation.
|
||||||
|
* @author mc_dev
|
||||||
|
*
|
||||||
|
* @param <V>
|
||||||
|
* @param <N>
|
||||||
|
*/
|
||||||
|
public static abstract class MapNode<V, N extends HashMapNode<V, N>> extends Node<V, N>{
|
||||||
|
protected Map<Integer, N> 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<N> getChildren(final int distance, final int range, final Collection<N> 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<Integer, N> newMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node using a simple HashMap.
|
||||||
|
* @author mc_dev
|
||||||
|
*
|
||||||
|
* @param <V>
|
||||||
|
* @param <N>
|
||||||
|
*/
|
||||||
|
public static class HashMapNode<V, N extends HashMapNode<V, N>> extends MapNode<V, N>{
|
||||||
|
/** Map Levenshtein distance to next nodes. */
|
||||||
|
protected int initialCapacity = 4;
|
||||||
|
protected float loadFactor = 0.75f;
|
||||||
|
public HashMapNode(V value) {
|
||||||
|
super(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Map<Integer, N> newMap() {
|
||||||
|
return new HashMap<Integer, N>(initialCapacity, loadFactor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SimpleNode<V> extends HashMapNode<V, SimpleNode<V>>{
|
||||||
|
public SimpleNode(V content) {
|
||||||
|
super(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static interface NodeFactory<V, N extends Node<V, N>>{
|
||||||
|
public N newNode(V value, N parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of a lookup.
|
||||||
|
* @author mc_dev
|
||||||
|
*
|
||||||
|
* @param <V>
|
||||||
|
* @param <N>
|
||||||
|
*/
|
||||||
|
public static class LookupEntry<V, N extends Node<V, N>>{
|
||||||
|
// 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<N> 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<N> nodes, N match, int distance, boolean isNew){
|
||||||
|
this.nodes = nodes;
|
||||||
|
this.match = match;
|
||||||
|
this.distance = distance;
|
||||||
|
this.isNew = isNew;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static interface LookupEntryFactory<V, N extends Node<V, N>, L extends LookupEntry<V, N>>{
|
||||||
|
public L newLookupEntry(Collection<N> nodes, N match, int distance, boolean isNew);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final NodeFactory<V, N> nodeFactory;
|
||||||
|
|
||||||
|
protected final LookupEntryFactory<V, N, L> resultFactory;
|
||||||
|
|
||||||
|
protected N root = null;
|
||||||
|
|
||||||
|
/** Set to true to have visit called */
|
||||||
|
protected boolean visit = false;
|
||||||
|
|
||||||
|
public BKModTree(NodeFactory<V, N> nodeFactory, LookupEntryFactory<V, N, L> 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<N> inRange = new LinkedList<N>();
|
||||||
|
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<N> open = new ArrayList<N>();
|
||||||
|
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);
|
||||||
|
|
||||||
|
}
|
@ -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<SimpleTimedLevenNode, STBKLResult> {
|
||||||
|
|
||||||
|
public static class STBKLResult extends LookupEntry<char[], SimpleTimedLevenNode>{
|
||||||
|
public STBKLResult(Collection<SimpleTimedLevenNode> nodes, SimpleTimedLevenNode match, int distance, boolean isNew) {
|
||||||
|
super(nodes, match, distance, isNew);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public SimpleTimedBKLevenshtein() {
|
||||||
|
super(
|
||||||
|
new NodeFactory<char[], SimpleTimedLevenNode>(){
|
||||||
|
@Override
|
||||||
|
public SimpleTimedLevenNode newNode( char[] value, SimpleTimedLevenNode parent) {
|
||||||
|
return new SimpleTimedLevenNode(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
,
|
||||||
|
new LookupEntryFactory<char[], SimpleTimedLevenNode, STBKLResult>() {
|
||||||
|
@Override
|
||||||
|
public STBKLResult newLookupEntry(Collection<SimpleTimedLevenNode> nodes, SimpleTimedLevenNode match, int distance, boolean isNew) {
|
||||||
|
return new STBKLResult(nodes, match, distance, isNew);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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<N extends TimedLevenNode<N>, L extends LookupEntry<char[], N>> extends BKLevenshtein<N, L> {
|
||||||
|
|
||||||
|
|
||||||
|
public static class TimedLevenNode<N extends TimedLevenNode<N>> extends LevenNode<N>{
|
||||||
|
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<SimpleTimedLevenNode>{
|
||||||
|
public SimpleTimedLevenNode(char[] value) {
|
||||||
|
super(value);
|
||||||
|
}
|
||||||
|
public SimpleTimedLevenNode(char[] value, long ts){
|
||||||
|
super(value, ts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimedBKLevenshtein(NodeFactory<char[], N> nodeFactory, LookupEntryFactory<char[], N, L> resultFactory) {
|
||||||
|
super(nodeFactory, resultFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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<Character> letters = new ArrayList<Character>(10);
|
|
||||||
protected final List<Character> digits = new ArrayList<Character>(10);
|
|
||||||
protected final List<Character> other = new ArrayList<Character>(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<Character> 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,5 +1,8 @@
|
|||||||
package fr.neatmonster.nocheatplus.checks.chat.analysis.engine;
|
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.ConfPaths;
|
||||||
import fr.neatmonster.nocheatplus.config.ConfigFile;
|
import fr.neatmonster.nocheatplus.config.ConfigFile;
|
||||||
|
|
||||||
@ -10,11 +13,35 @@ import fr.neatmonster.nocheatplus.config.ConfigFile;
|
|||||||
*/
|
*/
|
||||||
public class EnginePlayerConfig {
|
public class EnginePlayerConfig {
|
||||||
|
|
||||||
public final boolean ppComprWordsCheck;
|
public final boolean ppPrefixesCheck;
|
||||||
public final boolean ppWordFrequencyCheck;
|
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){
|
public EnginePlayerConfig(final ConfigFile config){
|
||||||
ppWordFrequencyCheck = config.getBoolean(ConfPaths.CHAT_GLOBALCHAT_ENGINE_PPWORDFREQ_CHECK, false);
|
// NOTE: These settings should be compared to the global settings done in the LetterEngine constructor.
|
||||||
ppComprWordsCheck = config.getBoolean(ConfPaths.CHAT_GLOBALCHAT_ENGINE_PPCOMPRWORDS_CHECK, false);
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,10 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import fr.neatmonster.nocheatplus.checks.chat.ChatConfig;
|
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.
|
* Engine specific player data.
|
||||||
@ -16,15 +20,12 @@ public class EnginePlayerData {
|
|||||||
|
|
||||||
public EnginePlayerData(ChatConfig cc) {
|
public EnginePlayerData(ChatConfig cc) {
|
||||||
EnginePlayerConfig config = cc.globalChatEnginePlayerConfig;
|
EnginePlayerConfig config = cc.globalChatEnginePlayerConfig;
|
||||||
if (config.ppWordFrequencyCheck){
|
if (config.ppWordsCheck)
|
||||||
// TODO: configure.
|
processors.add(new FlatWords("ppWords", config.ppWordsSettings));
|
||||||
processors.add(new FlatWordBuckets(50, 4, 1500, 0.9f));
|
if (config.ppPrefixesCheck)
|
||||||
}
|
processors.add(new WordPrefixes("ppPrefixes", config.ppPrefixesSettings));
|
||||||
if (config.ppComprWordsCheck){
|
if (config.ppSimilarityCheck)
|
||||||
// TODO: configure.
|
processors.add(new SimilarWordsBKL("ppSimilarity", config.ppSimilaritySettings));
|
||||||
processors.add(new CompressedWords(30000, 320, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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.ChatConfig;
|
||||||
import fr.neatmonster.nocheatplus.checks.chat.analysis.ds.ManagedMap;
|
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).
|
* Store EnginePlayerData. Expire data on get(String, Chatonfig).
|
||||||
@ -32,7 +33,7 @@ public class EnginePlayerDataMap extends ManagedMap<String, EnginePlayerData> {
|
|||||||
put(key, data);
|
put(key, data);
|
||||||
}
|
}
|
||||||
final long ts = System.currentTimeMillis();
|
final long ts = System.currentTimeMillis();
|
||||||
if (ts - durExpire > lastAccess) expire(ts - durExpire);
|
if (ts - lastAccess > durExpire) expire(ts - durExpire);
|
||||||
lastAccess = ts;
|
lastAccess = ts;
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
@ -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<String, ActionFrequency> 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<String, ActionFrequency>(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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,11 +1,22 @@
|
|||||||
package fr.neatmonster.nocheatplus.checks.chat.analysis.engine;
|
package fr.neatmonster.nocheatplus.checks.chat.analysis.engine;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
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.ChatConfig;
|
||||||
import fr.neatmonster.nocheatplus.checks.chat.ChatData;
|
import fr.neatmonster.nocheatplus.checks.chat.ChatData;
|
||||||
import fr.neatmonster.nocheatplus.checks.chat.analysis.MessageLetterCount;
|
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.ConfPaths;
|
||||||
import fr.neatmonster.nocheatplus.config.ConfigFile;
|
import fr.neatmonster.nocheatplus.config.ConfigFile;
|
||||||
|
|
||||||
@ -17,50 +28,65 @@ import fr.neatmonster.nocheatplus.config.ConfigFile;
|
|||||||
*/
|
*/
|
||||||
public class LetterEngine {
|
public class LetterEngine {
|
||||||
|
|
||||||
|
/** Global processors */
|
||||||
protected final List<WordProcessor> processors = new ArrayList<WordProcessor>();
|
protected final List<WordProcessor> processors = new ArrayList<WordProcessor>();
|
||||||
|
|
||||||
protected final EnginePlayerDataMap dataMap;
|
protected final EnginePlayerDataMap dataMap;
|
||||||
|
|
||||||
public LetterEngine(ConfigFile config){
|
public LetterEngine(ConfigFile config){
|
||||||
// Add word processors.
|
// Add word processors.
|
||||||
if (config.getBoolean(ConfPaths.CHAT_GLOBALCHAT_ENGINE_GLWORDFREQ_CHECK, false)){
|
// NOTE: These settings should be compared to the per player settings done in the EnginePlayerConfig constructor.
|
||||||
// TODO: Make aspects configurable.
|
if (config.getBoolean(ConfPaths.CHAT_GLOBALCHAT_GL_WORDS_CHECK, false)){
|
||||||
processors.add(new FlatWordBuckets(1000, 4, 1500, 0.9f));
|
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)){
|
if (config.getBoolean(ConfPaths.CHAT_GLOBALCHAT_GL_PREFIXES_CHECK , false)){
|
||||||
// TODO: Make aspects configurable.
|
WordPrefixesSettings settings = new WordPrefixesSettings();
|
||||||
processors.add(new CompressedWords(30000, 2000, false));
|
settings.maxAdd = 2000;
|
||||||
|
settings.applyConfig(config, ConfPaths.CHAT_GLOBALCHAT_GL_PREFIXES);
|
||||||
|
processors.add(new WordPrefixes("glPrefixes", settings));
|
||||||
}
|
}
|
||||||
|
if (config.getBoolean(ConfPaths.CHAT_GLOBALCHAT_GL_SIMILARITY_CHECK , false)){
|
||||||
// TODO: At least expiration duration configurable?
|
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);
|
dataMap = new EnginePlayerDataMap(600000L, 100, 0.75f);
|
||||||
}
|
}
|
||||||
|
|
||||||
public float process(final MessageLetterCount letterCount, final String playerName, final ChatConfig cc, final ChatData data){
|
public Map<String, Float> process(final MessageLetterCount letterCount, final String playerName, final ChatConfig cc, final ChatData data){
|
||||||
float score = 0;
|
|
||||||
|
final Map<String, Float> result = new HashMap<String, Float>();
|
||||||
|
|
||||||
// Global processors.
|
// Global processors.
|
||||||
for (final WordProcessor processor : processors){
|
for (final WordProcessor processor : processors){
|
||||||
final float refScore = processor.process(letterCount);
|
try{
|
||||||
|
result.put(processor.getProcessorName(), processor.process(letterCount) * cc.globalChatGlobalWeight);
|
||||||
// System.out.println("global:" + processor.getProcessorName() +": " + refScore);
|
}
|
||||||
|
catch( final Exception e){
|
||||||
score = Math.max(score, refScore);
|
Bukkit.getLogger().warning("[NoCheatPlus] globalchat: processor("+processor.getProcessorName()+") generated an exception: " + e.getClass().getSimpleName() + ": " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per player processors.
|
// Per player processors.
|
||||||
final EnginePlayerData engineData = dataMap.get(playerName, cc);
|
final EnginePlayerData engineData = dataMap.get(playerName, cc);
|
||||||
for (final WordProcessor processor : engineData.processors){
|
for (final WordProcessor processor : engineData.processors){
|
||||||
final float refScore = processor.process(letterCount);
|
try{
|
||||||
|
result.put(processor.getProcessorName(), processor.process(letterCount) * cc.globalChatPlayerWeight);
|
||||||
// System.out.println("player: " + processor.getProcessorName() +": " + refScore);
|
}
|
||||||
|
catch( final Exception e){
|
||||||
score = Math.max(score, refScore);
|
Bukkit.getLogger().warning("[NoCheatPlus] globalchat: processor("+processor.getProcessorName()+") generated an exception: " + e.getClass().getSimpleName() + ": " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
// TODO: Is max the right method?
|
|
||||||
|
|
||||||
return score;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clear() {
|
public void clear() {
|
||||||
|
@ -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.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -28,6 +28,9 @@ public abstract class AbstractWordProcessor implements WordProcessor{
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected String name;
|
protected String name;
|
||||||
|
/** Not set by constructor. */
|
||||||
|
protected float weight = 1f;
|
||||||
|
|
||||||
public AbstractWordProcessor(String name){
|
public AbstractWordProcessor(String name){
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
@ -37,6 +40,17 @@ public abstract class AbstractWordProcessor implements WordProcessor{
|
|||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getWeight() {
|
||||||
|
return weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWeight(float weight){
|
||||||
|
this.weight = weight;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public float process(final MessageLetterCount message) {
|
public float process(final MessageLetterCount message) {
|
||||||
// Does the looping, scores are summed up and divided by number of words.
|
// 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++){
|
for (int index = 0; index < message.words.length; index++){
|
||||||
final WordLetterCount word = message.words[index];
|
final WordLetterCount word = message.words[index];
|
||||||
final String key = word.word.toLowerCase();
|
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;
|
return score;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,7 +79,7 @@ public abstract class AbstractWordProcessor implements WordProcessor{
|
|||||||
* Process one word.
|
* Process one word.
|
||||||
* @param index
|
* @param index
|
||||||
* @param message
|
* @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);
|
public abstract float loop(final long ts, final int index, final String key, final WordLetterCount word);
|
||||||
}
|
}
|
@ -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<Character> letters = new ArrayList<Character>(10);
|
||||||
|
protected final List<Character> digits = new ArrayList<Character>(10);
|
||||||
|
protected final List<Character> other = new ArrayList<Character>(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<Character> chars;
|
||||||
|
if (compress) chars = word.counts.keySet();
|
||||||
|
else{
|
||||||
|
// Add all.
|
||||||
|
chars = new ArrayList<Character>(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<Character> 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<Character> chars, final long ts);
|
||||||
|
|
||||||
|
}
|
@ -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<String, ActionFrequency> 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<String, ActionFrequency>(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<Character> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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<Character> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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<Character> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
import fr.neatmonster.nocheatplus.checks.chat.analysis.MessageLetterCount;
|
||||||
|
|
||||||
@ -11,6 +11,12 @@ public interface WordProcessor{
|
|||||||
*/
|
*/
|
||||||
public String getProcessorName();
|
public String getProcessorName();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configured weight.
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public float getWeight();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param message
|
* @param message
|
@ -141,25 +141,39 @@ public abstract class ConfPaths {
|
|||||||
public static final String CHAT_COLOR_CHECK = CHAT_COLOR + "active";
|
public static final String CHAT_COLOR_CHECK = CHAT_COLOR + "active";
|
||||||
public static final String CHAT_COLOR_ACTIONS = CHAT_COLOR + "actions";
|
public static final String CHAT_COLOR_ACTIONS = CHAT_COLOR + "actions";
|
||||||
|
|
||||||
|
// globalchat
|
||||||
private static final String CHAT_GLOBALCHAT = CHAT + "globalchat.";
|
private static final String CHAT_GLOBALCHAT = CHAT + "globalchat.";
|
||||||
public static final String CHAT_GLOBALCHAT_CHECK = CHAT_GLOBALCHAT + "active";
|
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_LEVEL = CHAT_GLOBALCHAT + "level";
|
||||||
public static final String CHAT_GLOBALCHAT_ENGINE_CHECK = CHAT_GLOBALCHAT_ENGINE + "active";
|
public static final String CHAT_GLOBALCHAT_ENGINE_MAXIMUM = CHAT_GLOBALCHAT + "maximum";
|
||||||
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_FREQUENCY = CHAT_GLOBALCHAT + "frequency.";
|
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_WEIGHT = CHAT_GLOBALCHAT_FREQUENCY + "weight";
|
||||||
public static final String CHAT_GLOBALCHAT_FREQUENCY_FACTOR = CHAT_GLOBALCHAT_FREQUENCY + "factor";
|
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";
|
public static final String CHAT_GLOBALCHAT_ACTIONS = CHAT_GLOBALCHAT + "actions";
|
||||||
|
|
||||||
|
// nopwnage
|
||||||
private static final String CHAT_NOPWNAGE = CHAT + "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_CHECK = CHAT_NOPWNAGE + "active";
|
||||||
public static final String CHAT_NOPWNAGE_EXCLUSIONS = CHAT_NOPWNAGE + "exclusions";
|
public static final String CHAT_NOPWNAGE_EXCLUSIONS = CHAT_NOPWNAGE + "exclusions";
|
||||||
|
@ -128,21 +128,24 @@ public class DefaultConfig extends ConfigFile {
|
|||||||
set(ConfPaths.CHAT_COLOR_CHECK, true);
|
set(ConfPaths.CHAT_COLOR_CHECK, true);
|
||||||
set(ConfPaths.CHAT_COLOR_ACTIONS, "log:color:0:1:if cancel");
|
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_CHECK, true);
|
||||||
set(ConfPaths.CHAT_GLOBALCHAT_COMMANDS, new LinkedList<String>(Arrays.asList(
|
set(ConfPaths.CHAT_GLOBALCHAT_LEVEL, 80);
|
||||||
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_FREQUENCY_FACTOR, 0.9D);
|
set(ConfPaths.CHAT_GLOBALCHAT_FREQUENCY_FACTOR, 0.9D);
|
||||||
set(ConfPaths.CHAT_GLOBALCHAT_FREQUENCY_WEIGHT, 6);
|
set(ConfPaths.CHAT_GLOBALCHAT_FREQUENCY_WEIGHT, 6);
|
||||||
set(ConfPaths.CHAT_GLOBALCHAT_LEVEL, 80);
|
set(ConfPaths.CHAT_GLOBALCHAT_COMMANDS,
|
||||||
|
new LinkedList<String>(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");
|
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_CHECK, true);
|
||||||
set(ConfPaths.CHAT_NOPWNAGE_EXCLUSIONS, new ArrayList<String>());
|
set(ConfPaths.CHAT_NOPWNAGE_EXCLUSIONS, new ArrayList<String>());
|
||||||
set(ConfPaths.CHAT_NOPWNAGE_LEVEL, 500);
|
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_CHECK, true);
|
||||||
set(ConfPaths.CHAT_NOPWNAGE_CAPTCHA_CHARACTERS,
|
set(ConfPaths.CHAT_NOPWNAGE_CAPTCHA_CHARACTERS,
|
||||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890");
|
"abcdefghjkmnpqrtuvwxyzABCDEFGHJKMNPQRTUVWXYZ2346789");
|
||||||
set(ConfPaths.CHAT_NOPWNAGE_CAPTCHA_LENGTH, 6);
|
set(ConfPaths.CHAT_NOPWNAGE_CAPTCHA_LENGTH, 6);
|
||||||
set(ConfPaths.CHAT_NOPWNAGE_CAPTCHA_QUESTION,
|
set(ConfPaths.CHAT_NOPWNAGE_CAPTCHA_QUESTION,
|
||||||
"&cPlease type '&6[captcha]&c' to continue sending messages/commands.");
|
"&cPlease type '&6[captcha]&c' to continue sending messages/commands.");
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
package fr.neatmonster.nocheatplus.utilities;
|
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 java.util.logging.Logger;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.ChatColor;
|
import org.bukkit.ChatColor;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
@ -14,6 +19,17 @@ public class CheckUtils {
|
|||||||
|
|
||||||
/** The file logger. */
|
/** The file logger. */
|
||||||
public static Logger fileLogger = null;
|
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).
|
* 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];
|
return p[n];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Join parts with link.
|
||||||
|
* @param input
|
||||||
|
* @param link
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static <O extends Object> String join(final Collection<O> 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 <O extends Object> boolean scheduleOutputJoined(final List<O> 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.
|
* Removes the colors of a message.
|
||||||
|
Loading…
Reference in New Issue
Block a user