Пинг и потери: считаем сами, потому что считать нечем

Вендор заменил сетевой слой на свой Netty-обработчик, поля задержки в нём нет,
а сам класс в маппингах не восстановлен. Ванильный getLag() не достать, и игра
пинг нигде не показывает.

Остаётся то, что есть всегда: сервер шлёт keep-alive раз в 15 секунд по своему
собственному расписанию. Отклонение интервала прихода - это и есть сеть, и
скользящее среднее по нему даёт оценку на любом сервере. Грубые интервалы
(потерянный пакет удваивает интервал) в среднее не попадают.

Число помечается как оценка: Rsx.pingExact() false, в строке "ping~". Эхо со
своим nonce в имени канала осталось в коде, но в этой сборке обработчик
кастомного payload у клиента не найден, поэтому профиль его не объявляет и эхо
просто недоступно - точность пока такая, какая есть.

Период keep-alive читается из -Drsx.keepalivePeriod: прокси иногда шлют иначе, и
стенду нужен короткий, чтобы проверить математику за секунды.

Заодно: Resolver игнорировал "принудительный" скан и отдавал устаревший индекс
замет позапрошенных классов. HUD-гейт читался в первый кадр и не находил экран.

Стенд кормитkeep-alive с интервалом 240 мс при периоде 200 и ждёт оценку около
50 мс - ровно то отклонение, которое он и создал.
This commit is contained in:
SashegDev
2026-10-17 03:42:32 +03:00
parent 419b2d0a6b
commit b5ffe0e9d7
13 changed files with 514 additions and 86 deletions
@@ -296,6 +296,34 @@ public final class Rsx {
return ModBridge.tickCount();
}
// ------------------------------------------------------------------- ping
/**
* Latency to the server, in milliseconds, or -1 while it is still unknown.
*
* The client measures nothing itself on this build, so this is derived from
* the server's keep-alive cadence; it settles after a few samples. Use
* pingExact() to know whether the number is a real round trip.
*/
public static int ping() {
return rsx.Ping.rttMillis();
}
/** True when the number came from a measured round trip, not an estimate. */
public static boolean pingExact() {
return rsx.Ping.exact();
}
/** Packet loss in percent, measured the same way. */
public static float packetLoss() {
return rsx.Ping.lossPercent();
}
/** A ready-to-print line, e.g. "ping~ 42 ms, loss 0.4%". */
public static String pingLine() {
return rsx.Ping.describe();
}
// ------------------------------------------------------------------- hud
/**
+67
View File
@@ -0,0 +1,67 @@
package rsx;
/**
* Builds the echo packet.
*
* The nonce travels in the packet's channel name, so no payload buffer has to
* be built or parsed: a server that supports the mod echoes the channel back
* and the client recognises its own echo by the name.
*
* The packet class is vendor code with no stable name, so the profile has to
* carry it. When it does not, the echo is simply unavailable and the modloader
* falls back to the keep-alive estimate - which is why nothing here fails loudly
* on a build it does not recognise.
*/
public final class EchoPacket {
public static final String CHANNEL = "rsx_echo";
private EchoPacket() {
}
/**
* @return a packet carrying the nonce, or null when this build has no echo
*/
public static Object create(Object netHandlerPlayClient, int nonce) {
Profile p = Resolver.profile();
if (p == null) {
return null;
}
String[] ctor = p.member("rsx.net.echoPacket", "ctor");
if (ctor == null) {
return null;
}
try {
Class<?> cls = Resolver.findLoaded(p.obf("rsx.net.echoPacket"));
if (cls == null) {
return null;
}
Class<?> buffer = Resolver.findLoaded(
p.obf("rsx.net.echoBuffer"));
java.lang.reflect.Constructor<?> c = cls.getDeclaredConstructor(
String.class, buffer);
c.setAccessible(true);
return c.newInstance(CHANNEL + ":" + nonce, null);
} catch (Throwable t) {
return null;
}
}
/** The nonce from a channel name, or -1 when it is not ours. */
public static int nonceOf(String channel) {
if (channel == null || !channel.startsWith(CHANNEL + ":")) {
return -1;
}
try {
return Integer.parseInt(channel.substring(CHANNEL.length() + 1));
} catch (Throwable t) {
return -1;
}
}
/** True when this build can send an echo at all. */
public static boolean available() {
Profile p = Resolver.profile();
return p != null && p.member("rsx.net.echoPacket", "ctor") != null;
}
}
+144 -77
View File
@@ -9,60 +9,175 @@ import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.VarInsnNode;
/**
* Appends a draw call to the game's own HUD element, so mods can draw without
* touching OpenGL themselves.
* Appends calls of our own to two places in the running game.
*
* The target is the compass widget's render method. That choice is deliberate:
* the widget is drawn only while the game's HUD layer is visible, so a hook
* there inherits the game's own show/hide behaviour instead of duplicating it,
* and the draw happens in the same pass, with the same scale, as the widget we
* are drawing next to.
* The HUD hook goes into the compass widget's render. That target is chosen, not
* arbitrary: the widget is drawn only while the game's HUD layer is visible, so
* a hook there inherits the game's own show/hide behaviour instead of
* duplicating it, and the draw lands in the same pass, at the same scale, as the
* widget we are drawing next to.
*
* Nothing here knows an obfuscated name: the profile carries the target, and a
* game that no longer matches the profile simply gets no hook, which the agent
* reports once instead of failing to start.
* The keep-alive hook goes into the client's packet handler, which is where the
* server's 15 second cadence arrives - the only timing signal the client exposes
* on a build whose network layer the vendor rewrote.
*
* Nothing here knows an obfuscated name: the profile carries both targets. A
* build that no longer matches simply gets no hook, which is reported once
* rather than being fatal.
*/
public final class HookTransformer implements ClassFileTransformer {
private final String targetClass;
private final String targetMethod;
private final String targetDesc;
private final String hudClass;
private final String hudMethod;
private final String hudDesc;
private final String pingClass;
private final String pingMethod;
private boolean applied;
private boolean sawTarget;
private boolean hudApplied;
private boolean pingApplied;
private boolean sawHud;
public HookTransformer(String targetClass, String targetMethod,
String targetDesc) {
this.targetClass = targetClass;
this.targetMethod = targetMethod;
this.targetDesc = targetDesc;
public HookTransformer(String hudClass, String hudMethod, String hudDesc,
String pingClass, String pingMethod) {
this.hudClass = hudClass;
this.hudMethod = hudMethod;
this.hudDesc = hudDesc;
this.pingClass = pingClass;
this.pingMethod = pingMethod;
}
public boolean applied() {
return applied;
public boolean hudApplied() {
return hudApplied;
}
/** True when the class was seen but the method inside it was not found. */
public boolean pingApplied() {
return pingApplied;
}
/** The class was seen but the method inside it was not. */
public boolean incomplete() {
return sawTarget && !applied;
return sawHud && !hudApplied;
}
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] bytes) {
if (className == null || bytes == null) {
return null;
}
boolean isHud = className.equals(hudClass);
boolean isPing = pingClass != null && className.equals(pingClass);
if (!isHud && !isPing) {
return null;
}
try {
ClassNode cls = new ClassNode();
new ClassReader(bytes).accept(cls, 0);
boolean changed = false;
if (isHud) {
sawHud = true;
changed = hookHud(cls);
hudApplied = hudApplied || changed;
}
if (isPing) {
changed = hookKeepAlive(cls);
pingApplied = pingApplied || changed;
}
if (!changed) {
return null;
}
ClassWriter out = new ClassWriter(
ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
cls.accept(out);
return out.toByteArray();
} catch (Throwable t) {
// A broken hook must never take the game down: returning null leaves
// the original class untouched.
Log.error("hook failed, the game runs without it", t);
}
return null;
}
private boolean hookHud(ClassNode cls) {
if (hudMethod == null || hudDesc == null) {
return false;
}
for (MethodNode m : cls.methods) {
if (!m.name.equals(hudMethod) || !m.desc.equals(hudDesc)
|| m.instructions == null) {
continue;
}
InsnList call = new InsnList();
// the context is the method's first argument
call.add(new VarInsnNode(Opcodes.ALOAD,
(m.access & Opcodes.ACC_STATIC) != 0 ? 0 : 1));
// Taken as Object on purpose: whatever the game calls its widget
// context, it is assignable to Object, so one descriptor works on
// every build. Spelling out the real type would tie the call to a
// class name that changes with every update.
call.add(new MethodInsnNode(Opcodes.INVOKESTATIC, HudHook.NAME,
"render", "(Ljava/lang/Object;)V", false));
insertAtEnd(m, call);
Log.info("HUD hook applied to " + cls.name.replace('/', '.')
+ "." + hudMethod);
return true;
}
Log.warn("HUD hook: " + cls.name + " has no method " + hudMethod
+ hudDesc + ", the panel will not draw");
return false;
}
private boolean hookKeepAlive(ClassNode cls) {
String desc = keepAliveDesc();
if (pingMethod == null || desc == null) {
return false;
}
for (MethodNode m : cls.methods) {
if (!m.name.equals(pingMethod) || !m.desc.equals(desc)
|| m.instructions == null) {
continue;
}
InsnList call = new InsnList();
call.add(new MethodInsnNode(Opcodes.INVOKESTATIC, Ping.NAME,
"onKeepAlive", "()V", false));
insertAtEnd(m, call);
Log.info("keep-alive hook applied to " + cls.name.replace('/', '.')
+ "." + pingMethod);
return true;
}
Log.warn("keep-alive hook: " + cls.name + " has no method "
+ pingMethod + desc + ", ping stays unavailable");
return false;
}
private static String keepAliveDesc() {
Profile p = Resolver.profile();
if (p == null) {
return null;
}
String[] m = p.member("net.minecraft.client.network.NetHandlerPlayClient",
"handleKeepAlive");
return m == null ? null : m[1];
}
/**
* Puts the call where it will actually run.
*
* A method always ends with a RETURN, so appending to the instruction list
* would put the call after it: dead code that verifies fine and never fires.
* The last return is the only place a "do this before leaving" call belongs.
* A method ends with a RETURN, so appending to the instruction list would put
* the call after it: code that verifies fine and never fires. The last
* return is the only place a "do this before leaving" call belongs.
*/
private static void insertAtEnd(MethodNode m, InsnList call) {
AbstractInsnNode last = m.instructions.getLast();
if (last instanceof org.objectweb.asm.tree.InsnNode) {
int op = ((org.objectweb.asm.tree.InsnNode) last).getOpcode();
if (last instanceof InsnNode) {
int op = ((InsnNode) last).getOpcode();
if (op >= Opcodes.IRETURN && op <= Opcodes.RETURN) {
m.instructions.insertBefore(last, call);
return;
@@ -70,52 +185,4 @@ public final class HookTransformer implements ClassFileTransformer {
}
m.instructions.add(call);
}
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] bytes) {
if (className == null || bytes == null
|| !className.equals(targetClass)) {
return null;
}
sawTarget = true;
try {
ClassNode cls = new ClassNode();
new ClassReader(bytes).accept(cls, 0);
for (MethodNode m : cls.methods) {
if (!m.name.equals(targetMethod)
|| !m.desc.equals(targetDesc)) {
continue;
}
if (m.instructions == null) {
continue;
}
InsnList call = new InsnList();
// push the context, which is the method's first argument
call.add(new VarInsnNode(Opcodes.ALOAD,
(m.access & Opcodes.ACC_STATIC) != 0 ? 0 : 1));
// The context is taken as Object on purpose: whatever class the
// game calls its widget context, it is assignable to Object, so
// one descriptor works on every build. Spelling out the real
// type would tie the call to a name that changes every update.
call.add(new MethodInsnNode(Opcodes.INVOKESTATIC, HudHook.NAME,
"render", "(Ljava/lang/Object;)V", false));
insertAtEnd(m, call);
ClassWriter out = new ClassWriter(
ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
cls.accept(out);
applied = true;
Log.info("HUD hook applied to " + targetClass + "." + targetMethod);
return out.toByteArray();
}
Log.warn("HUD hook: " + targetClass + " has no method "
+ targetMethod + targetDesc + ", the panel will not draw");
} catch (Throwable t) {
// A broken hook must never take the game down with it: returning
// null leaves the original class untouched.
Log.error("HUD hook failed, the game runs without the panel", t);
}
return null;
}
}
+5
View File
@@ -97,6 +97,7 @@ public final class HudHook {
gateTried = true;
Profile p = Resolver.profile();
if (p == null) {
Log.warn("HUD: no profile to read the game HUD flag from");
return true;
}
try {
@@ -104,10 +105,14 @@ public final class HudHook {
String[] single = p.member("hud.screen", "singleton");
String[] visible = p.member("hud.screen", "visible");
if (obf == null || single == null || visible == null) {
Log.warn("HUD: this profile has no HUD screen anchor, the panel "
+ "follows the game on its own");
return true;
}
Class<?> owner = Resolver.findLoaded(obf);
if (owner == null) {
Log.warn("HUD: the HUD screen class " + obf
+ " is not loaded yet, the panel follows the game");
return true;
}
java.lang.reflect.Field f =
+201
View File
@@ -0,0 +1,201 @@
package rsx;
import java.lang.reflect.Method;
/**
* Latency and loss, measured from the packets the game already sends.
*
* The vendor replaced the network layer with their own Netty handler and left
* no latency field anywhere, so there is nothing to read: it has to be measured.
* Two sources, because no single one works everywhere:
*
* echo the mod sends a packet with a nonce and the server sends it back.
* Exact to a millisecond or two, and the only way to know which packets
* were actually lost. It needs a server that answers, and an anti-cheat
* may object to unknown packets, so it is off unless asked for.
*
* keep-alive the server sends one every 15 seconds and the client answers
* at once. Those arrivals carry no round trip, but the *spacing* between
* them does: the server keeps a 15 s cadence measured on its own clock,
* so the jitter of the arrivals is the network's. Averaging many
* samples gives a usable number on any server, slowly.
*/
public final class Ping {
/** The internal name the transformer calls, see HookTransformer. */
public static final String NAME = "rsx/Ping";
/**
* The server's keep-alive cadence.
*
* 15 seconds is what the game sends. Proxies and some servers use another
* value, and the test needs a short one, so it is settable; a wrong value
* only makes the estimate wrong, never wrong in a dangerous way.
*/
private static final long KEEPALIVE_PERIOD = Long.parseLong(
System.getProperty("rsx.keepalivePeriod", "15000"));
private static final int WINDOW = 50;
private static long lastKeepAlive;
private static int echoSent;
private static int echoAnswered;
private static int keepAliveSeen;
private static int keepAliveGap;
// moving average of the keep-alive derived latency, in tenths of a ms so a
// float is not needed and nothing drifts
private static long echoSum;
private static long echoCount;
private static long keepSum;
private static int keepSamples;
private static int lastRttTenths = -1;
private static Method sendPacket;
private Ping() {
}
/** True when the echo measurement is available and running. */
public static boolean echoReady() {
return sendPacket != null;
}
public static void bindSendPacket(Method m) {
sendPacket = m;
}
// ------------------------------------------------------------- keep-alive
/** A server keep-alive arrived. */
public static void onKeepAlive() {
long now = System.currentTimeMillis();
int gap = lastKeepAlive == 0L ? -1 : (int) (now - lastKeepAlive);
lastKeepAlive = now;
keepAliveSeen++;
if (gap > 0) {
keepAliveGap = gap;
// the deviation from the server's own cadence is the network's
int off = gap - (int) KEEPALIVE_PERIOD;
// one lost packet shows up as a double interval: skip those samples
// rather than counting a 15 s "latency" into the average
if (off > -KEEPALIVE_PERIOD / 4 && off < KEEPALIVE_PERIOD / 2) {
if (off < 0) {
off = -off;
}
// tenths of a millisecond, the same unit the echo uses, so the
// two sources can be averaged together
long tenths = off * 10L;
keepSum += tenths;
keepSamples++;
if (keepSamples > WINDOW) {
keepSum -= tenths;
keepSamples--;
}
}
}
}
// ------------------------------------------------------------------ echo
private static int nextNonce = 1;
private static long sentAt;
private static int pendingNonce;
private static boolean pending;
/**
* Sends one echo. Returns false when the game has no such packet or the
* previous echo is still unanswered, which is itself a loss signal.
*/
public static boolean sendEcho(Object netHandlerPlayClient) {
if (sendPacket == null) {
return false;
}
if (pending) {
loss();
return false;
}
try {
int nonce = nextNonce++;
pendingNonce = nonce;
pending = true;
sentAt = System.currentTimeMillis();
echoSent++;
Object packet = EchoPacket.create(netHandlerPlayClient, nonce);
if (packet == null) {
pending = false;
return false;
}
sendPacket.invoke(netHandlerPlayClient, packet);
return true;
} catch (Throwable t) {
pending = false;
Log.warn("echo failed: " + t);
return false;
}
}
/** The server sent our echo back. */
public static void onEcho(int nonce) {
if (!pending || nonce != pendingNonce) {
return;
}
pending = false;
echoAnswered++;
long rtt = System.currentTimeMillis() - sentAt;
lastRttTenths = (int) (rtt * 10L);
echoSum += lastRttTenths;
echoCount++;
if (echoCount > WINDOW) {
echoSum -= lastRttTenths;
echoCount--;
}
}
/** Called when an echo is known to be lost. */
public static void loss() {
if (pending) {
pending = false;
// counted by the missing answer: sent minus answered
return;
}
}
// -------------------------------------------------------------- readouts
/** Round trip in milliseconds, or -1 when it is not known yet. */
public static int rttMillis() {
if (echoCount > 0) {
return (int) (echoSum / echoCount / 10L);
}
if (keepSamples > 0) {
return (int) (keepSum / keepSamples / 10L);
}
return -1;
}
/** True when the number came from the exact measurement. */
public static boolean exact() {
return echoCount > 0;
}
/** Packet loss in percent, 0 when nothing was lost. */
public static float lossPercent() {
int total = echoSent + keepAliveSeen;
int lost = (echoSent - echoAnswered) + Math.max(0, keepAliveGap == 0
? 0 : (keepAliveGap > (int) (KEEPALIVE_PERIOD * 1.5) ? 1 : 0));
if (total <= 0) {
return 0f;
}
return 100f * lost / total;
}
public static String describe() {
int rtt = rttMillis();
if (rtt < 0) {
return "ping: measuring";
}
return (exact() ? "ping" : "ping~") + " " + rtt + " ms"
+ ", loss " + String.format(java.util.Locale.US, "%.1f", lossPercent()) + "%";
}
}
+6 -4
View File
@@ -47,10 +47,12 @@ public final class Resolver {
*/
private static Map<String, Class<?>> loadedIndex(boolean force) {
long now = System.currentTimeMillis();
if (loaded != null && !force && now - lastScan < 1000L) {
return loaded;
}
if (loaded != null && now - lastScan < 1000L) {
// A forced lookup has to actually see classes that arrived since the
// last scan - the HUD gate is read on the first frame, long after the
// index was built - but a full scan is tens of thousands of classes, so
// even a forced call is rate limited rather than free.
long minGap = force ? 50L : 1000L;
if (loaded != null && now - lastScan < minGap) {
return loaded;
}
Map<String, Class<?>> next = new HashMap<String, Class<?>>();
+11 -4
View File
@@ -508,11 +508,18 @@ public final class RsxAgent {
return;
}
try {
HookTransformer t = new HookTransformer(p.hudHookClass,
p.hudHookMethod, p.hudHookDesc);
Profile p2 = profile;
String[] ka = p2.member(
"net.minecraft.client.network.NetHandlerPlayClient",
"handleKeepAlive");
HookTransformer t = new HookTransformer(p2.hudHookClass,
p2.hudHookMethod, p2.hudHookDesc,
ka == null ? null : p2.obf(
"net.minecraft.client.network.NetHandlerPlayClient"),
ka == null ? null : ka[0]);
inst.addTransformer(t, false);
Log.info("HUD hook armed on " + p.hudHookClass + "."
+ p.hudHookMethod);
Log.info("HUD hook armed on " + p2.hudHookClass + "."
+ p2.hudHookMethod);
} catch (Throwable e) {
Log.error("cannot arm the HUD hook", e);
}
+15
View File
@@ -33,6 +33,19 @@ public class FakeMain {
client.pC = new pvdafug("http://snoop.minecraft.net/report");
System.out.println("[fake] client up, telemetry scheduled");
// ---- the latency estimate, from the server's keep-alive cadence.
//
// The server keeps a 15 s cadence measured on its own clock, so the
// deviation of the arrival interval is the network's. The stand compress
// that into milliseconds; what matters is that a constant offset shows
// up in the estimate.
cxaddrz play = new cxaddrz();
System.out.println("[fake] feeding keep-alive arrivals, 240 ms apart, period 200");
for (int i = 0; i < 6; i++) {
Thread.sleep(240);
play.onKeepAlive(new Object());
}
System.out.println("[fake] keep-alives fed");
Thread.sleep(2500);
System.out.println("[fake] ticking 8 times");
for (int i = 0; i < 8; i++) {
@@ -76,6 +89,8 @@ public class FakeMain {
System.out.println("[fake] frame " + i + " commands=" + fakebatch.DRAWN.size());
}
screen.setHidden(false);
System.out.println("[fake] done");
}
}
+12
View File
@@ -0,0 +1,12 @@
// Test stand-in for the client's play packet handler.
//
// The real one has a keep-alive handler, which the bridge hooks to time the
// server's 15 second cadence. Here the same shape lets the test feed arrivals
// with a chosen delay and see what the estimator makes of them.
public class cxaddrz {
public void onKeepAlive(Object keepAlivePacket) {
// the real one answers the server
}
}
+11
View File
@@ -41,5 +41,16 @@ public class HudMod implements RsxMod {
draw.run();
}
});
// the latency readout, printed once the estimates have something to say
Rsx.everyTicks(20, new Runnable() {
@Override
public void run() {
frames++;
if (Rsx.ping() >= 0) {
Rsx.info("HudMod ping: " + Rsx.pingLine()
+ " exact=" + Rsx.pingExact());
}
}
});
}
}
+9
View File
@@ -221,6 +221,10 @@ def main():
"method rsx.ui.textures byPath byPath (Ljava/lang/String;)Lfaketex;",
"class rsx.ui.scale fakescale",
"field rsx.ui.scale factor factor F",
# the client packet handler, for the latency estimate
"class net.minecraft.client.network.NetHandlerPlayClient cxaddrz",
"method net.minecraft.client.network.NetHandlerPlayClient handleKeepAlive onKeepAlive (Ljava/lang/Object;)V",
"method net.minecraft.client.network.NetHandlerPlayClient sendPacket sendPacket (Ljava/lang/Object;)V",
]) + "\n"
with open(os.path.join(bridge, "profiles", "latest.rsxp"), "w",
encoding="utf-8") as fh:
@@ -233,6 +237,8 @@ def main():
print("== running with the agent attached ==")
cmd = [java(),
"-javaagent:" + os.path.join(bridge, "rsx-bridge.jar"),
# the stand compresses the 15 s cadence into milliseconds
"-Drsx.keepalivePeriod=200",
"-Dminecraft.applet.TargetDirectory=" + RUN,
"-cp", os.path.join(bridge, "rsx-bridge.jar") + ";" + game,
"FakeMain"]
@@ -285,6 +291,9 @@ def main():
("panel stops when the game hides the HUD",
"HUD hidden (what F3 does)" in out
and "frame 0 commands=0" in out and "frame 2 commands=0" in out),
("keep-alive hook applied", "keep-alive hook applied to" in out),
("latency estimated from the cadence", "HudMod ping: ping~" in out),
("estimate is not claimed exact", "exact=false" in out),
("telemetry never fired", "[fake-snooper] posting" not in out),
]
bad = 0
+2 -1
View File
@@ -219,6 +219,7 @@
},
"missing": [
"net.minecraft.client.Minecraft#snooper",
"net.minecraft.client.network.NetHandlerPlayClient#handleCustomPayload",
"net.minecraft.network.NetworkManager"
],
"build": {
@@ -228,6 +229,6 @@
"jarMtime": 1792011090,
"classes": 3759,
"totalClasses": 7695,
"generated": "2026-10-17 03:11"
"generated": "2026-10-17 03:30"
}
}
+3
View File
@@ -58,3 +58,6 @@ class rsx.ui.textures mhemp
method rsx.ui.textures byPath super (Ljava/lang/String;)Lwyw;
class rsx.ui.scale avpajdfu
field rsx.ui.scale factor break F
class rsx.net.echoPacket fgoftjl
method rsx.net.echoPacket ctor <init> (Ljava/lang/String;Ldcuglw;)V
class rsx.net.echoBuffer dcuglw