Package fr.neatmonster.nocheatplus.checks.fight

Source Code of fr.neatmonster.nocheatplus.checks.fight.FightListener

package fr.neatmonster.nocheatplus.checks.fight;

import java.util.Iterator;

import org.bukkit.Location;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.entity.TNTPrimed;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityDamageEvent.DamageCause;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.event.entity.EntityRegainHealthEvent;
import org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason;
import org.bukkit.event.player.PlayerAnimationEvent;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerItemHeldEvent;
import org.bukkit.event.player.PlayerToggleSprintEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;

import fr.neatmonster.nocheatplus.NCPAPIProvider;
import fr.neatmonster.nocheatplus.checks.CheckListener;
import fr.neatmonster.nocheatplus.checks.CheckType;
import fr.neatmonster.nocheatplus.checks.combined.Combined;
import fr.neatmonster.nocheatplus.checks.combined.Improbable;
import fr.neatmonster.nocheatplus.checks.inventory.Items;
import fr.neatmonster.nocheatplus.checks.moving.LocationTrace;
import fr.neatmonster.nocheatplus.checks.moving.LocationTrace.TraceEntry;
import fr.neatmonster.nocheatplus.checks.moving.MediumLiftOff;
import fr.neatmonster.nocheatplus.checks.moving.MovingConfig;
import fr.neatmonster.nocheatplus.checks.moving.MovingData;
import fr.neatmonster.nocheatplus.checks.moving.MovingListener;
import fr.neatmonster.nocheatplus.compat.BridgeHealth;
import fr.neatmonster.nocheatplus.components.JoinLeaveListener;
import fr.neatmonster.nocheatplus.permissions.Permissions;
import fr.neatmonster.nocheatplus.stats.Counters;
import fr.neatmonster.nocheatplus.utilities.TickTask;
import fr.neatmonster.nocheatplus.utilities.TrigUtil;
import fr.neatmonster.nocheatplus.utilities.build.BuildParameters;

/**
* Central location to listen to events that are relevant for the fight checks.<br>
* This listener is registered after the CombinedListener.
*
* @see FightEvent
*/
public class FightListener extends CheckListener implements JoinLeaveListener{

    /** The angle check. */
    private final Angle       angle       = addCheck(new Angle());

    /** The critical check. */
    private final Critical    critical    = addCheck(new Critical());

    /** The direction check. */
    private final Direction   direction   = addCheck(new Direction());
   
    /** Faster health regeneration check. */
    private final FastHeal fastHeal      = addCheck(new FastHeal());

    /** The god mode check. */
    private final GodMode     godMode     = addCheck(new GodMode());

    /** The knockback check. */
    private final Knockback   knockback   = addCheck(new Knockback());

    /** The no swing check. */
    private final NoSwing     noSwing     = addCheck(new NoSwing());

    /** The reach check. */
    private final Reach       reach       = addCheck(new Reach());
   
    /** The self hit check */
    private final SelfHit     selfHit     = addCheck(new SelfHit());

    /** The speed check. */
    private final Speed       speed       = addCheck(new Speed());
   
    /** For temporary use: LocUtil.clone before passing deeply, call setWorld(null) after use. */
  private final Location useLoc1 = new Location(null, 0, 0, 0);
 
  /** For temporary use: LocUtil.clone before passing deeply, call setWorld(null) after use. */
  private final Location useLoc2 = new Location(null, 0, 0, 0);
 
  private final Counters counters = NCPAPIProvider.getNoCheatPlusAPI().getGenericInstance(Counters.class);
    private final int idCancelDead = counters.registerKey("canceldead");
   
    public FightListener(){
      super(CheckType.FIGHT);
    }

    /**
     * A player attacked something with DamageCause ENTITY_ATTACK. That's most likely what we want to really check.
     *
     * @param event
     *            The EntityDamageByEntityEvent
     * @return
     */
    private boolean handleNormalDamage(final Player player, final Entity damaged, final double damage, final int tick, final FightData data) {
        final FightConfig cc = FightConfig.getConfig(player);
       
        // Hotfix attempt for enchanted books.
        // TODO: maybe a generaluzed version for the future...
        final ItemStack stack = player.getItemInHand();
        // Illegal enchantments hotfix check.
        if (Items.checkIllegalEnchantments(player, stack)) return true;
       
        boolean cancelled = false;
       
        final String worldName = player.getWorld().getName();
        final long now = System.currentTimeMillis();
        final boolean worldChanged = !worldName.equals(data.lastWorld);
       
        final Location loc =  player.getLocation(useLoc1);
//        // Bad pitch/yaw, just in case.
//     if (LocUtil.needsDirectionCorrection(useLoc1.getYaw(), useLoc1.getPitch())) {
//       mcAccess.correctDirection(player);
//       player.getLocation(useLoc1);
//     }
        final Location damagedLoc = damaged.getLocation(useLoc2);
//        final double targetDist = CheckUtils.distance(loc, targetLoc); // TODO: Calculate distance as is done in fight.reach !
        final double targetMove;
        final int tickAge;
        final long msAge; // Milliseconds the ticks actually took.
        final double normalizedMove; // Blocks per second.
        // TODO: relative distance (player - target)!
        // TODO: Use trace for this ?
        if (data.lastAttackedX == Double.MAX_VALUE || tick < data.lastAttackTick || worldChanged || tick - data.lastAttackTick > 20){
          // TODO: 20 ?
          tickAge = 0;
          targetMove = 0.0;
          normalizedMove = 0.0;
          msAge = 0;
        }
        else{
          tickAge = tick - data.lastAttackTick;
          // TODO: Maybe use 3d distance if dy(normalized) is too big.
          targetMove = TrigUtil.distance(data.lastAttackedX, data.lastAttackedZ, damagedLoc.getX(), damagedLoc.getZ());
          msAge = (long) (50f * TickTask.getLag(50L * tickAge, true) * (float) tickAge);
          normalizedMove = msAge == 0 ? targetMove : targetMove * Math.min(20.0, 1000.0 / (double) msAge);
        }
        // TODO: calculate factor for dists: ticks * 50 * lag
       
        // TODO: dist < width => skip some checks (direction, ..)
     
        final LocationTrace damagedTrace;
        final Player damagedPlayer;
        if (damaged instanceof Player){
          damagedPlayer = (Player) damaged;
//          // Bad pitch/yaw, just in case.
//         if (LocUtil.needsDirectionCorrection(useLoc2.getYaw(), useLoc2.getPitch())) {
//           mcAccess.correctDirection(damagedPlayer);
//           damagedPlayer.getLocation(useLoc2);
//         }
         // Log.
          if (cc.debug && damagedPlayer.hasPermission(Permissions.ADMINISTRATION_DEBUG)){
            damagedPlayer.sendMessage("Attacked by " + player.getName() + ": inv=" + mcAccess.getInvulnerableTicks(damagedPlayer) + " ndt=" + damagedPlayer.getNoDamageTicks());
          }
          // Check for self hit exploits (mind that projectiles are excluded from this.)
          if (selfHit.isEnabled(player) && selfHit.check(player, damagedPlayer, data, cc)) {
            cancelled = true;
          }
          // Get+update the damaged players.
          // TODO: Problem with NPCs: data stays (not a big problem).
          // (This is done even if the event has already been cancelled, to keep track, if the player is on a horse.)
          damagedTrace = MovingData.getData(damagedPlayer).updateTrace(damagedPlayer, damagedLoc, tick);
        } else {
          damagedPlayer = null; // TODO: This is a temporary workaround.
          // Use a fake trace.
          // TODO: Provide for entities too? E.g. one per player, or a fully fledged bookkeeping thing (EntityData).
          //final MovingConfig mcc = MovingConfig.getConfig(damagedLoc.getWorld().getName());
          damagedTrace = null; //new LocationTrace(mcc.traceSize, mcc.traceMergeDist);
          //damagedTrace.addEntry(tick, damagedLoc.getX(), damagedLoc.getY(), damagedLoc.getZ());
        }
       
        if (cc.cancelDead){
          if (damaged.isDead()) {
            cancelled = true;
          }
          // Only allow damaging others if taken damage this tick.
            if (player.isDead() && data.damageTakenByEntityTick != TickTask.getTick()){
              cancelled = true;
            }
        }
       
        if (damage <= 4.0 && tick == data.damageTakenByEntityTick && data.thornsId != Integer.MIN_VALUE && data.thornsId == damaged.getEntityId()){
          // Don't handle further, but do respect selfhit/canceldead.
          // TODO: Remove soon.
          data.thornsId = Integer.MIN_VALUE;
          return cancelled;
        }
        else {
          data.thornsId = Integer.MIN_VALUE;
        }

        // Run through the main checks.
        if (!cancelled && speed.isEnabled(player)){
          if (speed.check(player, now)){
            cancelled = true;
            // Still feed the improbable.
            if (data.speedVL > 50){
              Improbable.check(player, 2f, now, "fight.speed");
            }
            else{
              Improbable.feed(player, 2f, now);
            }
          }
          else if (normalizedMove > 2.0 && Improbable.check(player, 1f, now, "fight.speed")){
            // Feed improbable in case of ok-moves too.
            // TODO: consider only feeding if attacking with higher average speed (!)
            cancelled = true;
          }
        }

        if (!cancelled && critical.isEnabled(player) && critical.check(player, loc, data, cc)) {
          cancelled = true;
        }
       
        if (!cancelled && knockback.isEnabled(player) && knockback.check(player, data, cc)) {
          cancelled = true;
        }
       
        if (!cancelled && noSwing.isEnabled(player) && noSwing.check(player, data, cc)) {
          cancelled = true;
        }
       
        if (!cancelled && player.isBlocking() && !player.hasPermission(Permissions.MOVING_SURVIVALFLY_BLOCKING)) {
          cancelled = true;
        }
       
        // TODO: Order of all these checks ...
        // Checks that use LocationTrace.

        // TODO: Later optimize (...), should reverse check window ?
       
        // First loop through reach and direction, to determine a window.
        final boolean reachEnabled = !cancelled && reach.isEnabled(player);
        final boolean directionEnabled = !cancelled && direction.isEnabled(player);
       
        if (reachEnabled || directionEnabled) {
          if (damagedPlayer != null) {
            // TODO: Move to a method (trigonometric checks).
                final ReachContext reachContext = reachEnabled ? reach.getContext(player, loc, damaged, damagedLoc, data, cc) : null;
                final DirectionContext directionContext = directionEnabled ? direction.getContext(player, loc, damaged, damagedLoc, data, cc) : null;
               
                final long traceOldest = tick; // - damagedTrace.getMaxSize(); // TODO: Set by window.
                // TODO: Iterating direction: could also start from latest, be it on occasion.
                Iterator<TraceEntry> traceIt = damagedTrace.maxAgeIterator(traceOldest);
               
                boolean violation = true; // No tick with all checks passed.
                boolean reachPassed = !reachEnabled; // Passed individually for some tick.
                boolean directionPassed = !directionEnabled; // Passed individually for some tick.
                // TODO: Maintain a latency estimate + max diff and invalidate completely (i.e. iterate from latest NEXT time)], or just max latency.
                while (traceIt.hasNext()) {
                  final TraceEntry entry = traceIt.next();
                  // Simplistic just check both until end or hit.
                  // TODO: Other default distances/tolerances.
                  boolean thisPassed = true;
                  if (reachEnabled) {
                    if (reach.loopCheck(player, loc, damagedPlayer, entry, reachContext, data, cc)) {
                      thisPassed = false;
                    } else {
                      reachPassed = true;
                    }
                  }
                  // TODO: For efficiency one could omit checking at all if reach is failed all the time.
                  if (directionEnabled && (reachPassed || !directionPassed)) {
                    if (direction.loopCheck(player, damagedLoc, damagedPlayer, entry, directionContext, data, cc)) {
                      thisPassed = false;
                    } else {
                      directionPassed = true;
                    }
                  }
                  if (thisPassed) {
                    // TODO: Log/set estimated latency.
                    violation = false;
                    break;
                  }
                }
                // TODO: How to treat mixed state: violation && reachPassed && directionPassed [current: use min violation // thinkable: silent cancel, if actions have cancel (!)]
                // TODO: Adapt according to strictness settings?
                if (reachEnabled) {
                  // TODO: Might ignore if already cancelled by mixed/silent cancel.
                  if (reach.loopFinish(player, loc, damagedPlayer, reachContext, violation, data, cc)) {
                    cancelled = true;
                  }
                }
                if (directionEnabled) {
                  // TODO: Might ignore if already cancelled.
                  if (direction.loopFinish(player, loc, damagedPlayer, directionContext, violation, data, cc)) {
                    cancelled = true;
                  }
                }
                // TODO: Log exact state, probably record min/max latency (individually).
          } else {
            // Still use the classic methods for non-players. maybe[]
            if (reachEnabled && reach.check(player, loc, damaged, damagedLoc, data, cc)) {
                  cancelled = true;
                }
               
                if (directionEnabled && direction.check(player, loc, damaged, damagedLoc, data, cc)) {
                  cancelled = true;
                }
          }
        }
       
        // Check angle with allowed window.
        if (angle.isEnabled(player)) {
          // TODO: Revise, use own trace.
      // The "fast turning" checks are checked in any case because they accumulate data.
      // Improbable yaw changing: Moving events might be missing up to a ten degrees change.
      if (Combined.checkYawRate(player, loc.getYaw(), now, worldName, cc.yawRateCheck)) {
        // (Check or just feed).
        // TODO: Work into this somehow attacking the same aim and/or similar aim position (not cancel then).
        cancelled = true;
      }
      // Angle check.
      if (angle.check(player, worldChanged, data, cc)) {
        if (!cancelled && cc.debug) {
          System.out.println(player.getName() + " fight.angle cancel without yawrate cancel.");
        }
        cancelled = true;
      }
    }
       
        // Set values.
        data.lastWorld = worldName;
      data.lastAttackTick = tick;
      data.lastAttackedX = damagedLoc.getX();
      data.lastAttackedY = damagedLoc.getY();
      data.lastAttackedZ = damagedLoc.getZ();
//      data.lastAttackedDist = targetDist;
     
      // Care for the "lost sprint problem": sprint resets, client moves as if still...
      // TODO: Use stored distance calculation same as reach check?
      // TODO: For pvp: make use of "player was there" heuristic later on.
      // TODO: Confine further with simple pre-conditions.
      // TODO: Evaluate if moving traces can help here.
      if (!cancelled && TrigUtil.distance(loc.getX(), loc.getZ(), damagedLoc.getX(), damagedLoc.getZ()) < 4.5){
        final MovingData mData = MovingData.getData(player);
      // Check if fly checks is an issue at all, re-check "real sprinting".
        if (mData.fromX != Double.MAX_VALUE && mData.mediumLiftOff != MediumLiftOff.LIMIT_JUMP){
          final double hDist = TrigUtil.distance(loc.getX(), loc.getZ(), mData.fromX, mData.fromZ);
          if (hDist >= 0.23) {
            // TODO: Might need to check hDist relative to speed / modifiers.
            final MovingConfig mc = MovingConfig.getConfig(player);
            if (now <= mData.timeSprinting + mc.sprintingGrace && MovingListener.shouldCheckSurvivalFly(player, mData, mc)){
              // Judge as "lost sprint" problem.
              // TODO: What would mData.lostSprintCount > 0  mean here?
                mData.lostSprintCount = 7;
                if ((cc.debug || mc.debug) && BuildParameters.debugLevel > 0){
                  System.out.println(player.getName() + " (lostsprint) hDist to last from: " + hDist + " | targetdist=" + TrigUtil.distance(loc.getX(), loc.getZ(), damagedLoc.getX(), damagedLoc.getZ()) + " | sprinting=" + player.isSprinting() + " | food=" + player.getFoodLevel() +" | hbuf=" + mData.sfHorizontalBuffer);
                }
            }
          }
        }
      }
     
      // Generic attacking penalty.
      // (Cancel after sprinting hacks, because of potential fp).
        if (!cancelled && data.attackPenalty.isPenalty(now)) {
          cancelled = true;
          if (cc.debug) {
            System.out.println(player.getName() + " ~ attack penalty.");
          }
        }
       
      // Cleanup.
        useLoc1.setWorld(null);
        useLoc2.setWorld(null);
       
        return cancelled;
    }
   
    /**
     * Check if a player might return some damage due to the "thorns" enchantment.
     * @param player
     * @return
     */
    public static final boolean hasThorns(final Player player){
      final PlayerInventory inv = player.getInventory();
      final ItemStack[] contents = inv.getArmorContents();
      for (int i = 0; i < contents.length; i++){
        final ItemStack stack = contents[i];
        if (stack != null && stack.getEnchantmentLevel(Enchantment.THORNS) > 0){
          return true;
        }
      }
      return false;
    }

    /**
     * We listen to EntityDamage events for obvious reasons.
     *
     * @param event
     *            the event
     */
    @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
    public void onEntityDamage(final EntityDamageEvent event) {
     
      final Entity damaged = event.getEntity();
      final Player damagedPlayer = damaged instanceof Player ? (Player) damaged : null;
      final FightData damagedData = damagedPlayer == null ? null : FightData.getData(damagedPlayer);
      final boolean damagedIsDead = damaged.isDead();
      if (damagedPlayer != null && !damagedIsDead) {
            if (!damagedPlayer.isDead() && godMode.isEnabled(damagedPlayer) && godMode.check(damagedPlayer, BridgeHealth.getDamage(event), damagedData)){
                // It requested to "cancel" the players invulnerability, so set their noDamageTicks to 0.
              damagedPlayer.setNoDamageTicks(0);
            }
            if (BridgeHealth.getHealth(damagedPlayer) >= BridgeHealth.getMaxHealth(damagedPlayer)){
              // TODO: Might use the same FightData instance for GodMode.
              if (damagedData.fastHealBuffer < 0){
                // Reduce negative buffer with each full health.
                damagedData.fastHealBuffer /= 2;
              }
              // Set reference time.
              damagedData.fastHealRefTime = System.currentTimeMillis();
            }
        }
//      System.out.println(event.getCause());
      // Attacking entities.
        if (event instanceof EntityDamageByEntityEvent) {
            final EntityDamageByEntityEvent e = (EntityDamageByEntityEvent) event;
            final Entity damager = e.getDamager();
            final int tick = TickTask.getTick();
          if (damagedPlayer != null && !damagedIsDead){
              // TODO: check once more when to set this (!) in terms of order.
            FightData.getData(damagedPlayer).damageTakenByEntityTick = tick;
                if (hasThorns(damagedPlayer)){
                // TODO: Cleanup here.
                  // Remember the id of the attacker to allow counter damage.
                  damagedData.thornsId = damager.getEntityId();
              }
                else{
                  damagedData.thornsId = Integer.MIN_VALUE;
                }
          }
          final DamageCause damageCause = event.getCause();
          final Player player = damager instanceof Player ? (Player) damager : null;
          Player attacker = player;
          // TODO: deobfuscate.
          if (damager instanceof TNTPrimed) {
            final Entity source = ((TNTPrimed) damager).getSource();
            if (source instanceof Player) {
              attacker = (Player) source;
            }
          }
          if (attacker != null && (damageCause == DamageCause.BLOCK_EXPLOSION || damageCause == DamageCause.ENTITY_EXPLOSION)) {
            // NOTE: Pigs don't have data.
        final FightData data = FightData.getData(attacker);
              data.lastExplosionEntityId = damaged.getEntityId();
          data.lastExplosionDamageTick = tick;
          return;
        }
            if (player != null){
                final double damage = BridgeHealth.getDamage(e);
                final FightData data = FightData.getData(player);
                if (damageCause == DamageCause.ENTITY_ATTACK){
            // TODO: Might/should skip the damage comparison, though checking on lowest priority.
                  if (damaged.getEntityId() == data.lastExplosionEntityId && tick == data.lastExplosionDamageTick) {
                    data.lastExplosionDamageTick = -1;
                    data.lastExplosionEntityId = Integer.MAX_VALUE;
                  } else if (handleNormalDamage(player, damaged, damage, tick, data)){
                    e.setCancelled(true);
                  }
                }
            }
        }
    }
   
    @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
    public void onEntityDamageMonitor(final EntityDamageEvent event) {
      final Entity damaged = event.getEntity();
      if (damaged instanceof Player){
        final Player player = (Player) damaged;
        final FightData data = FightData.getData(player);
        final int ndt = player.getNoDamageTicks();
        if (data.lastDamageTick == TickTask.getTick() && data.lastNoDamageTicks != ndt){
          // Plugin compatibility thing.
          data.lastNoDamageTicks = ndt;
        }
      }
    }

    /**
     * We listen to death events to prevent a very specific method of doing godmode.
     *
     * @param event
     *            the event
     */
    @EventHandler(priority = EventPriority.MONITOR)
    protected void onEntityDeathEvent(final EntityDeathEvent event) {
        // Only interested in dying players.
        final Entity entity = event.getEntity();
        if (entity instanceof Player){
            final Player player = (Player) entity;
            if (godMode.isEnabled(player)) {
              godMode.death(player);
            }
        }
    }

    /**
     * We listen to PlayerAnimation events because it is used for arm swinging.
     *
     * @param event
     *            the event
     */
    @EventHandler(priority = EventPriority.MONITOR)
    protected void onPlayerAnimation(final PlayerAnimationEvent event) {
        // Set a flag telling us that the arm has been swung.
        FightData.getData(event.getPlayer()).noSwingArmSwung = true;
    }

    /**
     * We listen to the PlayerToggleSprint events for the Knockback check.
     *
     * @param event
     *            the event
     */
    @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
    public void onPlayerToggleSprint(final PlayerToggleSprintEvent event) {
        if (event.isSprinting()) {
          FightData.getData(event.getPlayer()).knockbackSprintTime = System.currentTimeMillis();
        }
    }
   
    @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
    public void onEntityRegainHealthLow(final EntityRegainHealthEvent event){
      final Entity entity = event.getEntity();
      if (!(entity instanceof Player)) return;
      final Player player = (Player) entity;
      if (player.isDead() && BridgeHealth.getHealth(player) <= 0.0) {
        // Heal after death.
        event.setCancelled(true);
        counters.addPrimaryThread(idCancelDead, 1);
        return;
      }
      if (event.getRegainReason() != RegainReason.SATIATED) {
        return;
      }
      if (fastHeal.isEnabled(player) && fastHeal.check(player)) {
        // TODO: Can clients force events with 0-re-gain ?
        event.setCancelled(true);
      }
    }
   
    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void onEntityRegainHealth(final EntityRegainHealthEvent event){
      final Entity entity = event.getEntity();
      if (!(entity instanceof Player)) return;
      final Player player = (Player) entity;
      final FightData data = FightData.getData(player);
      // Adjust god mode data:
      // Remember the time.
      data.regainHealthTime = System.currentTimeMillis();
      // Set god-mode health to maximum.
      // TODO: Mind that health regain might half the ndt.
      final double health = Math.min(BridgeHealth.getHealth(player) + BridgeHealth.getAmount(event), BridgeHealth.getMaxHealth(player));
      data.godModeHealth = Math.max(data.godModeHealth, health);
    }

  @Override
  public void playerJoins(final Player player) {
  }

  @Override
  public void playerLeaves(final Player player) {
    final FightData data = FightData.getData(player);
    data.angleHits.clear();
  }
 
  @EventHandler(priority = EventPriority.MONITOR)
    public void onPlayerChangedWorld(final PlayerChangedWorldEvent event){
    FightData.getData(event.getPlayer()).onWorldChange();
  }
 
  @EventHandler(ignoreCancelled = false, priority = EventPriority.MONITOR)
    public void onItemHeld(final PlayerItemHeldEvent event) {
    final Player player = event.getPlayer();
    final long penalty = FightConfig.getConfig(player).toolChangeAttackPenalty;
    if (penalty > 0 ) {
      FightData.getData(player).attackPenalty.applyPenalty(penalty);
    }
  }

}
TOP

Related Classes of fr.neatmonster.nocheatplus.checks.fight.FightListener

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.