package javastory.channel.anticheat;
import java.awt.Point;
import java.lang.ref.WeakReference;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import javastory.channel.ChannelCharacter;
import javastory.game.GameConstants;
import javastory.server.TimerManager;
import javastory.tools.StringUtil;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
public class CheatTracker {
private final Map<CheatingOffense, CheatingOffenseEntry> offenses = Maps.newConcurrentMap();
private final WeakReference<ChannelCharacter> chr;
// For keeping track of speed attack hack.
private int lastAttackTickCount = 0;
private byte Attack_tickResetCount = 0;
private long Server_ClientAtkTickDiff = 0;
private long lastDamage = 0;
private long takingDamageSince;
private int numSequentialDamage = 0;
private long lastDamageTakenTime = 0;
private byte numZeroDamageTaken = 0;
private int numSequentialSummonAttack = 0;
private long summonSummonTime = 0;
private int numSameDamage = 0;
private Point lastMonsterMove;
private int monsterMoveCount;
private int attacksWithoutHit = 0;
private ScheduledFuture<?> invalidationTask;
public CheatTracker(final ChannelCharacter chr) {
this.chr = new WeakReference<>(chr);
this.invalidationTask = TimerManager.getInstance().register(new InvalidationTask(), 60000);
this.takingDamageSince = System.currentTimeMillis();
}
public final void checkAttack(final int skillId, final int tickcount) {
final short AtkDelay = GameConstants.getAttackDelay(skillId);
if (tickcount - this.lastAttackTickCount < AtkDelay) {
this.registerOffense(CheatingOffense.FASTATTACK);
}
final long STime_TC = System.currentTimeMillis() - tickcount; // hack =
// -
// more
if (this.Server_ClientAtkTickDiff - STime_TC > 250) { // 250 is the ping,
// TODO
this.registerOffense(CheatingOffense.FASTATTACK2);
}
// if speed hack, client tickcount values will be running at a faster
// pace
// For lagging, it isn't an issue since TIME is running simotaniously,
// client
// will be sending values of older time
// System.out.println("Delay [" + skillId + "] = " + (tickcount -
// lastAttackTickCount) + ", " + (Server_ClientAtkTickDiff - STime_TC));
this.Attack_tickResetCount++; // Without this, the difference will always be
// at 100
if (this.Attack_tickResetCount >= (AtkDelay <= 200 ? 2 : 4)) {
this.Attack_tickResetCount = 0;
this.Server_ClientAtkTickDiff = STime_TC;
}
this.lastAttackTickCount = tickcount;
}
public final void checkTakeDamage(final int damage) {
this.numSequentialDamage++;
this.lastDamageTakenTime = System.currentTimeMillis();
// System.out.println("tb" + timeBetweenDamage);
// System.out.println("ns" + numSequentialDamage);
// System.out.println(timeBetweenDamage / 1500 + "(" + timeBetweenDamage
// / numSequentialDamage + ")");
if (this.lastDamageTakenTime - this.takingDamageSince / 500 < this.numSequentialDamage) {
this.registerOffense(CheatingOffense.FAST_TAKE_DAMAGE);
}
if (this.lastDamageTakenTime - this.takingDamageSince > 4500) {
this.takingDamageSince = this.lastDamageTakenTime;
this.numSequentialDamage = 0;
}
/*
* (non-thieves)
* Min Miss Rate: 2%
* Max Miss Rate: 80%
* (thieves)
* Min Miss Rate: 5%
* Max Miss Rate: 95%
*/
if (damage == 0) {
this.numZeroDamageTaken++;
if (this.numZeroDamageTaken >= 35) { // Num count MSEA a/b players
this.numZeroDamageTaken = 0;
this.registerOffense(CheatingOffense.HIGH_AVOID);
}
} else if (damage != -1) {
this.numZeroDamageTaken = 0;
}
}
public final void checkSameDamage(final int dmg) {
if (dmg > 1 && this.lastDamage == dmg) {
this.numSameDamage++;
if (this.numSameDamage > 5) {
this.numSameDamage = 0;
this.registerOffense(CheatingOffense.SAME_DAMAGE, this.numSameDamage + " times: " + dmg);
}
} else {
this.lastDamage = dmg;
this.numSameDamage = 0;
}
}
public final void checkMoveMonster(final Point pos) {
if (pos == this.lastMonsterMove) {
this.monsterMoveCount++;
if (this.monsterMoveCount > 15) {
this.registerOffense(CheatingOffense.MOVE_MONSTERS);
}
} else {
this.lastMonsterMove = pos;
this.monsterMoveCount = 1;
}
}
public final void resetSummonAttack() {
this.summonSummonTime = System.currentTimeMillis();
this.numSequentialSummonAttack = 0;
}
public final boolean checkSummonAttack() {
this.numSequentialSummonAttack++;
// estimated
// System.out.println(numMPRegens + "/" + allowedRegens);
if ((System.currentTimeMillis() - this.summonSummonTime) / (2000 + 1) < this.numSequentialSummonAttack) {
this.registerOffense(CheatingOffense.FAST_SUMMON_ATTACK);
return false;
}
return true;
}
public final int getAttacksWithoutHit() {
return this.attacksWithoutHit;
}
public final void setAttacksWithoutHit(final boolean increase) {
if (increase) {
this.attacksWithoutHit++;
} else {
this.attacksWithoutHit = 0;
}
}
public final void registerOffense(final CheatingOffense offense) {
this.registerOffense(offense, null);
}
public final void registerOffense(final CheatingOffense offense, final String param) {
final ChannelCharacter character = this.chr.get();
if (character == null || !offense.isEnabled()) {
return;
}
CheatingOffenseEntry entry = this.offenses.get(offense);
if (entry != null && entry.isExpired()) {
this.expireEntry(entry);
entry = null;
}
if (entry == null) {
entry = new CheatingOffenseEntry(offense, character.getId());
}
if (param != null) {
entry.setParam(param);
}
entry.incrementCount();
if (offense.shouldAutoban(entry.getCount())) {
final byte type = offense.getBanType();
if (type == 1) {
// AutobanManager.getInstance().autoban(chrhardref.getClient(),
// StringUtil.makeEnumHumanReadable(offense.name()));
} else if (type == 2) {
if (!character.isGM()) {
// chrhardref.getClient().disconnect();
} else {
character.sendNotice(5, "[WARNING] D/c triggred : " + offense.toString());
}
}
return;
}
this.offenses.put(offense, entry);
CheatingOffensePersister.getInstance().persistEntry(entry);
}
public final void expireEntry(final CheatingOffenseEntry coe) {
this.offenses.remove(coe.getOffense());
}
public final int getPoints() {
int ret = 0;
CheatingOffenseEntry[] offenses_copy;
offenses_copy = this.offenses.values().toArray(new CheatingOffenseEntry[this.offenses.size()]);
for (final CheatingOffenseEntry entry : offenses_copy) {
if (entry.isExpired()) {
this.expireEntry(entry);
} else {
ret += entry.getPoints();
}
}
return ret;
}
public final Map<CheatingOffense, CheatingOffenseEntry> getOffenses() {
return Collections.unmodifiableMap(this.offenses);
}
public final String getSummary() {
final StringBuilder ret = new StringBuilder();
final List<CheatingOffenseEntry> offenseList = Lists.newArrayList();
for (final CheatingOffenseEntry entry : this.offenses.values()) {
if (!entry.isExpired()) {
offenseList.add(entry);
}
}
Collections.sort(offenseList, new Comparator<CheatingOffenseEntry>() {
@Override
public final int compare(final CheatingOffenseEntry o1, final CheatingOffenseEntry o2) {
final int thisVal = o1.getPoints();
final int anotherVal = o2.getPoints();
return thisVal < anotherVal ? 1 : thisVal == anotherVal ? 0 : -1;
}
});
final int to = Math.min(offenseList.size(), 4);
for (int x = 0; x < to; x++) {
ret.append(StringUtil.makeEnumHumanReadable(offenseList.get(x).getOffense().name()));
ret.append(": ");
ret.append(offenseList.get(x).getCount());
if (x != to - 1) {
ret.append(" ");
}
}
return ret.toString();
}
public final void dispose() {
if (this.invalidationTask != null) {
this.invalidationTask.cancel(false);
}
this.invalidationTask = null;
}
private final class InvalidationTask implements Runnable {
@Override
public final void run() {
CheatingOffenseEntry[] offenses_copy;
synchronized (CheatTracker.this.offenses) {
offenses_copy = CheatTracker.this.offenses.values().toArray(new CheatingOffenseEntry[CheatTracker.this.offenses.size()]);
}
for (final CheatingOffenseEntry offense : offenses_copy) {
if (offense.isExpired()) {
CheatTracker.this.expireEntry(offense);
}
}
if (CheatTracker.this.chr.get() == null) {
CheatTracker.this.dispose();
}
}
}
}