Sign in Sign up
kretrod/lodestone Public
Branches
master
110 lines (104 loc) · 4.1 KB Raw
package lodestone;

import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.concurrent.EventExecutor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

/**
 * Backtrack: hold one entity's movement packets back for a fixed delay, then
 * let them through, so that entity renders where it was a moment ago.
 *
 * The point is the server's own lag compensation. When you attack, the server
 * rewinds the target to where your latency says you last saw it and checks the
 * hit against that rewound box — so a hit on the delayed position still lands,
 * as long as the delay stays inside the window the server is willing to rewind.
 * Nothing here is sent to the server; it only changes when inbound packets are
 * applied on our side.
 *
 * Only the current target's movement is delayed. Everything else — every other
 * entity, every non-movement packet — is passed on the instant it arrives, so
 * the world at large is untouched.
 */
public final class Backtrack extends ChannelDuplexHandler {
    public static volatile boolean enabled = false;
    public static volatile int targetId = -1;
    /** Bounded to the server's rewind window; past it the hit is refused. */
    public static volatile long delayMs = 120;

    // Movement packets carry their entity in different shapes across the
    // protocol; resolve the getter once per class and cache it.
    private static final java.util.Map<Class<?>, Object> ACCESSORS =
        new java.util.concurrent.ConcurrentHashMap<>();
    private static final Object NONE = new Object();

    private static int entityIdOf(Object msg) {
        Class<?> c = msg.getClass();
        Object acc = ACCESSORS.get(c);
        if (acc == null) {
            acc = resolveAccessor(c);
            ACCESSORS.put(c, acc);
        }
        if (acc == NONE) {
            return -1;
        }
        try {
            if (acc instanceof Method) {
                return (Integer) ((Method) acc).invoke(msg);
            }
            return ((Field) acc).getInt(msg);
        } catch (Throwable t) {
            return -1;
        }
    }

    private static Object resolveAccessor(Class<?> c) {
        String name = c.getSimpleName();
        // Only these carry per-entity movement; nothing else is a candidate.
        boolean isMovement = name.contains("MoveEntity")
            || name.contains("EntityPositionSync")
            || name.contains("TeleportEntity")
            || name.contains("RotateHead")
            || name.contains("SetEntityMotion");
        if (!isMovement) {
            return NONE;
        }
        for (String m : new String[] {"id", "entityId", "getId"}) {
            try {
                Method method = c.getMethod(m);
                if (method.getReturnType() == int.class) {
                    method.setAccessible(true);
                    return method;
                }
            } catch (NoSuchMethodException ignored) {
            }
        }
        for (Class<?> k = c; k != null; k = k.getSuperclass()) {
            for (String f : new String[] {"entityId", "id"}) {
                try {
                    Field field = k.getDeclaredField(f);
                    if (field.getType() == int.class) {
                        field.setAccessible(true);
                        return field;
                    }
                } catch (NoSuchFieldException ignored) {
                }
            }
        }
        return NONE;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (!enabled || targetId < 0 || entityIdOf(msg) != targetId) {
            super.channelRead(ctx, msg);
            return;
        }
        // Re-fire on the same event loop after the delay, so ordering and
        // thread-affinity match what netty would have done itself.
        EventExecutor exec = ctx.executor();
        long delay = Math.max(0, Math.min(delayMs, 1000));
        exec.schedule(() -> {
            try {
                ctx.fireChannelRead(msg);
            } catch (Throwable ignored) {
            }
        }, delay, TimeUnit.MILLISECONDS);
    }
}