1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
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);
}
}