/* * This file is part of Hawk Anticheat. * Copyright (C) 2018 Hawk Development Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ package me.islandscout.hawk.module; import me.islandscout.hawk.util.*; import me.islandscout.hawk.Hawk; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Location; import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.entity.*; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.block.BlockPistonExtendEvent; import org.bukkit.util.Vector; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; public class LagCompensator implements Listener { //https://developer.valvesoftware.com/wiki/Lag_compensation //https://www.youtube.com/watch?v=6EwaW2iz4iA //http://www.gabrielgambetta.com/lag-compensation.html //http://www.gabrielgambetta.com/client-side-prediction-live-demo.html //https://en.wikipedia.org/wiki/Lag#Rewind_time //Yes, I use System.currentTimeMillis()! Should the main thread start lagging //behind, the lag compensator will not be affected. The lag compensator //should be dependent on system time and not server ticks. System time is //stable for this application and is thus a reliable time reference for //measuring latency. private final Map<Entity, List<Pair<Location, Long>>> trackedEntities; private final List<PistonPush> pistonPushes; private final int historySize; private final int pingOffset; private final boolean DEBUG; private final int SEARCH_WIDTH; private final int SEARCH_HEIGHT; private final int POLL_RATE; private final boolean ALWAYS_TICK_PLAYERS; public LagCompensator(Hawk hawk) { this.trackedEntities = new ConcurrentHashMap<>(); this.pistonPushes = new CopyOnWriteArrayList<>(); historySize = ConfigHelper.getOrSetDefault(20, hawk.getConfig(), "lagCompensation.historySize"); pingOffset = ConfigHelper.getOrSetDefault(175, hawk.getConfig(), "lagCompensation.pingOffset"); SEARCH_WIDTH = ConfigHelper.getOrSetDefault(40, hawk.getConfig(), "lagCompensation.entityTracking.searchWidth") / 2; SEARCH_HEIGHT = ConfigHelper.getOrSetDefault(20, hawk.getConfig(), "lagCompensation.entityTracking.searchHeight") / 2; POLL_RATE = ConfigHelper.getOrSetDefault(20, hawk.getConfig(), "lagCompensation.entityTracking.searchRate"); ALWAYS_TICK_PLAYERS = ConfigHelper.getOrSetDefault(true, hawk.getConfig(), "lagCompensation.entityTracking.alwaysTickPlayers"); DEBUG = ConfigHelper.getOrSetDefault(false, hawk.getConfig(), "lagCompensation.debug"); Bukkit.getPluginManager().registerEvents(this, hawk); hawk.getHawkSyncTaskScheduler().addRepeatingTask(new Runnable() { @Override public void run() { Set<Entity> collectedEntities = new HashSet<>(); for(Player p : Bukkit.getOnlinePlayers()) { List<Entity> nearbyEntities = p.getNearbyEntities(SEARCH_WIDTH, SEARCH_HEIGHT, SEARCH_WIDTH); for(Entity entity : nearbyEntities) { //add anything that moves and is clickable if(entity instanceof LivingEntity || entity instanceof Vehicle || entity instanceof Fireball) { collectedEntities.add(entity); } } } for(Entity entity : collectedEntities) { trackedEntities.put(entity, trackedEntities.getOrDefault(entity, new CopyOnWriteArrayList<>())); } Set<Entity> expiredEntities = new HashSet<>(trackedEntities.keySet()); expiredEntities.removeAll(collectedEntities); for(Entity expired : expiredEntities) { trackedEntities.remove(expired); } } }, POLL_RATE); if(ALWAYS_TICK_PLAYERS) { hawk.getHawkSyncTaskScheduler().addRepeatingTask(new Runnable() { @Override public void run() { for(Player p : Bukkit.getOnlinePlayers()) { trackedEntities.put(p, trackedEntities.getOrDefault(p, new CopyOnWriteArrayList<>())); } } }, 1); } hawk.getHawkSyncTaskScheduler().addRepeatingTask(new Runnable() { @Override public void run() { for(Entity entity : trackedEntities.keySet()) { processPosition(entity); } /* for(HawkPlayer pp : hawk.getHawkPlayers()) { Player p = pp.getPlayer(); p.getActivePotionEffects(); }*/ long currTime = System.currentTimeMillis(); while(pistonPushes.size() > 0 && currTime - pistonPushes.get(0).getTimestamp() > 2000) { pistonPushes.remove(0); } } }, 1); } //Uses linear interpolation to get the best location public Location getHistoryLocation(int rewindMillisecs, Entity entity) { List<Pair<Location, Long>> times = trackedEntities.get(entity); if (times == null || times.size() == 0) { return entity.getLocation(); } long currentTime = System.currentTimeMillis(); int rewindTime = rewindMillisecs + pingOffset; //player a + avg processing time. for (int i = times.size() - 1; i >= 0; i--) { //loop backwards int elapsedTime = (int) (currentTime - times.get(i).getValue()); if (elapsedTime >= rewindTime) { if (i == times.size() - 1) { return times.get(i).getKey(); } double nextMoveWeight = (elapsedTime - rewindTime) / (double) (elapsedTime - (currentTime - times.get(i + 1).getValue())); Location before = times.get(i).getKey().clone(); Location after = times.get(i + 1).getKey(); Vector interpolate = after.toVector().subtract(before.toVector()); interpolate.multiply(nextMoveWeight); before.add(interpolate); return before; } } return times.get(0).getKey().clone(); //ran out of historical data; return oldest entry } public Vector getHistoryVelocity(int rewindMillisecs, Entity entity) { List<Pair<Location, Long>> times = trackedEntities.get(entity); if (times == null || times.size() == 0) { return new Vector(0, 0, 0); } long currentTime = System.currentTimeMillis(); int rewindTime = rewindMillisecs + pingOffset; //player a + avg processing time. for (int i = times.size() - 1; i >= 0; i--) { //loop backwards int elapsedTime = (int) (currentTime - times.get(i).getValue()); if (elapsedTime >= rewindTime) { if (i == times.size() - 1) { return new Vector(0, 0, 0); } Location before = times.get(i).getKey(); Location after = times.get(i + 1).getKey(); return after.toVector().subtract(before.toVector()); } } return new Vector(0, 0, 0); //ran out of historical data; return oldest entry } public Location getHistoryLocationNoLerp(int rewindMillisecs, Entity entity) { List<Pair<Location, Long>> times = trackedEntities.get(entity); if (times == null || times.size() == 0) { return entity.getLocation(); } long currentTime = System.currentTimeMillis(); int rewindTime = rewindMillisecs + pingOffset; //player a + avg processing time. for (int i = times.size() - 1; i >= 0; i--) { //loop backwards int elapsedTime = (int) (currentTime - times.get(i).getValue()); if (elapsedTime >= rewindTime) { if (i == times.size() - 1) { return times.get(i).getKey(); } return times.get(i).getKey().clone(); } } return times.get(0).getKey().clone(); //ran out of historical data; return oldest entry } private void processPosition(Entity entity) { List<Pair<Location, Long>> times = trackedEntities.getOrDefault(entity, new CopyOnWriteArrayList<>()); long currTime = System.currentTimeMillis(); if(DEBUG && entity instanceof Player) { Player p = (Player) entity; p.sendMessage(ChatColor.GRAY + "[Lag Compensator] Your moves are being recorded: " + currTime); } times.add(new Pair<>(entity.getLocation(), currTime)); if (times.size() > historySize) times.remove(0); trackedEntities.put(entity, times); } public int getHistorySize() { return historySize; } public int getPingOffset() { return pingOffset; } public Set<Entity> getPositionTrackedEntities() { return trackedEntities.keySet(); } public List<Pair<Location, Long>> getHistoryPositionsOfEntity(Entity entity) { return trackedEntities.get(entity); } public boolean testNearPiston(Vector pos, World world, int ping) { long rewoundTime = System.currentTimeMillis() - ping; AABB playerBox = AABB.playerCollisionBox.clone(); playerBox.translate(pos); playerBox.expand(1, 1, 1); for(PistonPush pistonPush : pistonPushes) { if(pistonPush.getWorld().equals(world)) { if(Math.abs(pistonPush.getTimestamp() - rewoundTime) < 500) { Vector blockMin = pistonPush.getPosition().clone(); Vector blockMax = pistonPush.getPosition().clone().add(new Vector(1, 1, 1)); AABB basicBounds = new AABB(blockMin, blockMax); if(playerBox.isColliding(basicBounds)) { return true; } } } } return false; } @EventHandler public void pistonExtend(BlockPistonExtendEvent e) { World world = e.getBlock().getWorld(); BlockFace bf = e.getDirection(); Vector offset = new Vector(0, 0, 0); switch (bf) { case UP: offset.setY(1); break; case DOWN: offset.setY(-1); break; case EAST: offset.setX(1); break; case WEST: offset.setX(-1); break; case NORTH: offset.setZ(-1); break; case SOUTH: offset.setZ(1); } long currTime = System.currentTimeMillis(); pistonPushes.add(new PistonPush(world, e.getBlock().getLocation().toVector().add(offset), bf, currTime)); for(Block b : e.getBlocks()) { pistonPushes.add(new PistonPush(world, b.getLocation().toVector().add(offset), bf, currTime)); } } }