A special thank you to blurite/rsprot for the packet mappings.
Turns a RuneLite injected-client JAR into a readable copy you can decompile,
and publishes the game's packet tables as plain TSV files.
Two things you can do with it:
- Deobfuscate a client. Rename every class, field and method to something stable and readable, strip the annotations that break decompilers, and record each original name so you can map back.
- Read the packet tables. For revisions 235 to 240 the packet ids, names,
lengths and field layouts are already extracted into
data/, for both directions. You do not have to run anything to use those.
The output is for reading, not for playing. One pass rewrites three lazy string-concatenation sites into ordinary bytecode so decompilers can handle them, which changes when those constants are computed. Keep your original JAR and do not log in with the output.
RuneLite injects a named menuAction into client, but it is a thin wrapper.
The real work happens in a gamepack method whose name carries no information.
In RuneLite 1.12.34 that method is ef on class qp:
// before — javap of client, inside menuAction
public void menuAction(int, int, net.runelite.api.MenuAction, int, int, String, String);
...
60: bipush -44
62: invokestatic qp.ef:(IIIIIILjava/lang/String;Ljava/lang/String;IIB)VNothing about qp.ef tells you it is the menu action handler, and both names
change with every build. After a run:
// after — javap of client, same method
public void menuAction(int, int, net.runelite.api.MenuAction, int, int, String, String);
...
60: bipush -44
62: invokestatic classQP.method9682:(IIIIIILjava/lang/String;Ljava/lang/String;IIB)V| Before | After | What it is |
|---|---|---|
qp |
classQP |
the class holding the handler |
ef |
method9682 |
the handler itself |
Every original is recorded in an annotation, so nothing is lost and you can map back to the build you started from:
@ObfuscatedName("qp")
public class classQP {
@ObfuscatedName("ef")
@ObfuscatedSignature(descriptor = "(IIIIIILjava/lang/String;Ljava/lang/String;IIB)V")
public static final void method9682(
int, int, int, int, int, int, String, String, int, int, byte);
}The names are stable rather than descriptive — method9682 is a number, not a
word. What it buys you is a name that holds still while you work, and a way back
to ef when you need it. That trailing byte parameter is a garbage argument
the client passes and ignores; you can see it pushed as -44 at the call site.
Measured on RuneLite 1.12.34, from its audit report:
| Before | After | |
|---|---|---|
Poison @Named annotations |
26,217 | 0 |
| Exception-table entries in packet handlers | 1,640 | 10 |
| Nested constant-dynamic concat sites | 3 | 0 |
| Classes in the archive | 735 | 735 |
| Classes renamed | — | 696 |
| Packet names applied | — | 252 |
The same input always produces a byte-identical output, so you can check a result against someone else's by hash.
The client stores packets as numbered fields with no names. The tool reads the table out of the bytecode, applies the published names, and writes the field layout it found:
before an obfuscated static field holding a packet with id 0 and length 6,
and a buffer method per field with a one- or two-letter name
after 0 EVENT_MOUSE_CLICK_V1 6 ...
data/rev-240/mappings/client-packets.tsv — packets the client sends:
id name length write_count fields
1 IF_BUTTONX 9 4 W4[r 24,v,r 8,r 16](bz),W2[r 8,v](bv),W2[r 8,v](bv),W1[v](uq)
2 OPWORLDENTITY4 3 2 W2[r 8,v](bv),W1[s 0](dj)
3 RESUME_P_OBJDIALOG 2 1 W2[r 8,v](bv)
data/rev-240/mappings/server-packets.tsv — packets the server sends:
id name length read_count fields
0 CAM_TARGET_V4 5 2 I1(lt),I4(eb)
1 IF_SETOBJECT 10 3 I2(hn),I4(eb),I4(eb)
2 MESSAGE_FRIENDCHANNEL -1 5 X1(dc),J0(ci),I2(sr),I3(yz),I1(cf)
data/rev-239/mappings/client-mapping-to-240.tsv — the same packet across two
revisions, which is how you carry work forward when ids change:
source_id target_id source_name target_name fields
0 101 OPNPC6 OPNPC6 W2[r 8,v](bd)
1 32 SEND_PING_REPLY SEND_PING_REPLY W4[r 16,r 24,v,r 8](et),...
2 100 CLANSETTINGS_FULL_REQUEST CLANSETTINGS_FULL_REQUEST W1[v](bw)
| Column | Meaning |
|---|---|
id |
The number on the wire. |
name |
The published name, or - where none exists. |
length |
Bytes of payload. -1 means a one-byte length prefix, -2 a two-byte one. |
write_count / read_count |
How many buffer operations make up the payload. |
fields |
The payload, in order. |
A field looks like W2[r 8,v](bv) when the client writes it, or I4(eb) when
it reads one:
W2— a write of 2 bytes. The number is how many bytes the buffer method stores.I4— a read of 4 bytes returning anint. The letter is the buffer method's Java return type:Iint,Jlong,Zboolean,Bbyte,Vvoid, andXfor anything else, mostly strings and arrays. The number is how many bytes it takes off the buffer, soJ0andV0read none directly and do their work through a helper.[r 8,v]— how each byte is derived from the value, one entry per byte written.vis the value as-is,r 8is the value shifted right 8 bits (so the second byte of a larger number),a 128is the value plus 128,s 0is 0 minus the value, andstrnis a string. These encodings change between revisions, which is part of why a payload from one revision does not match the next.(bv)— the obfuscated name of the buffer method, so you can find it in the jar.
Nothing to run — these are in data/. Every name is checked against the
published protocol tables, and all of them match.
| Revision | RuneLite builds | Client packets | Server packets |
|---|---|---|---|
| 235 | 1.11.16 – 1.11.19 | 101 | 141 |
| 236 | 1.11.20 – 1.11.23 | 101 | 135 |
| 237 | 1.12.15 – 1.12.20 | 101 | 143 |
| 238 | 1.12.21 – 1.12.24 | 117 | 139 |
| 239 | 1.12.30 – 1.12.33 | 117 | 149 |
| 240 | 1.12.34 – 1.12.36-SNAPSHOT | 117 | 152 |
Revisions 239 and 240 leave 16 client entries unnamed. Those are table slots for packets that were removed: the published protocol does not name them and no code in the client sends them.
A new RuneLite version does not mean a new protocol. Several builds share a revision. Builds are also not interchangeable within a revision-sized group — 1.11.24 through 1.12.14 have the same number of client packets as revision 236 but assign different ids, and are a different protocol.
- Java 17 or newer. Set
JAVA_HOMEif your default JDK is older. - The checked-in
./gradlewwrapper (Gradle 8.12.1). - The build uses a Java 17 toolchain.
1. Get an official client JAR. Never a copy from elsewhere.
curl -O https://repo.runelite.net/net/runelite/injected-client/1.12.34/injected-client-1.12.34.jar2. Build the tool.
./gradlew :transform-jar:shadedJar3. Run it. --packet-profile is optional. Without it you still get a full
deobfuscation; you just get generic fieldN names on the packet fields instead
of real ones. There is a profile for every revision in the table above — pick the
one matching the revision your build belongs to.
java -jar transform-jar/build/libs/rl-suite-1.3.1-all.jar \
--input injected-client-1.12.34.jar \
--output injected-client-1.12.34-deob.jar \
--report audit.json \
--packet-profile osrs-240 \
--semantic-map semantic.tsv4. Decompile the output with the bundled Vineflower 1.11.2 integration:
java -jar transform-jar/build/libs/rl-suite-1.3.1-all.jar \
--decompile \
--input injected-client-1.12.34-deob.jar \
--source-output injected-client-1.12.34-sourcesThe source directory is replaced only after decompilation succeeds. The verified
deobfuscated JAR is not modified. Checked-in decompiled snapshots are under
semantics/.
Options:
| Option | Purpose |
|---|---|
--input, --output |
Required. The input is never modified. |
--report |
Writes an audit JSON with every metric shown above. |
--packet-profile |
osrs-235 through osrs-240, renamed-client-1.12.31.1, or none. Names the packet fields; everything else is unaffected. Auto-selected only for inputs whose checksum is known, so pass it. |
--semantic-map |
Optional checksum-bound map of reviewed class, field, and method names. Without it, deterministic generic names are used. |
--expected-malformed-named--expected-packet-handlers--expected-packet-ranges--expected-condy-sites |
Pin a count. The run fails if the input does not match, instead of guessing. |
Revisions 235 to 238 need --expected-packet-handlers. The tool looks for
five packet readers by default, which is what 239 and 240 have. Older clients
have fewer, and the run stops rather than guessing. The failure tells you the
number it found:
Expected 5 packet-handler shapes, found 4
Pass that number back as --expected-packet-handlers 4 and the run proceeds.
The count is a property of the build, not of the revision — two builds inside one revision can differ. Every build tested so far:
| Revision | Packet readers | Names applied |
|---|---|---|
| 235 | 4 | 242 |
| 236 | 3 | 236 |
| 237 | 4, except 1.12.20 which has 5 | 243 |
| 238 | 4, except 1.12.22.1 which has 5 | 256 |
| 239 | 5 (the default, nothing to pass) | 250 |
| 240 | 5, except 1.12.37-SNAPSHOT which has 6 | 252 |
Each build's own count is recorded as packet.handlers.matched in its audit
report under data/rev-<n>/audit/.
A packet profile is a name list, nothing more. The packet table itself — every
id and length — is read straight out of the bytecode, so the tool finds packets
in a revision it has never seen. The profiles are ordinary TSV files checked in
under transform-jar/src/main/resources, one per revision, named
osrs-<revision>.tsv. Nothing is downloaded and there is no runtime dependency
on anything outside this repository. Each file records in its own header where
its names were obtained, so a name can be traced back and rechecked.
The tool stops rather than guessing. A changed annotation shape, packet handler geometry, packet table, or unknown attribute ends the run before anything is written. A JAR from a revision it has not seen is refused at one of three points, in this order:
| Refusal | What it means | What to do |
|---|---|---|
| Unrecognised input checksum | The build is not one whose checksum is known, so no profile is picked for you. Raised before the JAR is parsed. | Pass --packet-profile explicitly. |
| Packet fingerprint matches nothing | The wire protocol revision changed. Every bundled profile fails identically, so this is a different protocol, not a tolerance problem. | Compare the id and length table in the JAR against the profile TSVs to see which revision it really is. |
| No packet handlers found | The five packet readers are located by an exact exception-table size, and that size changed. | Take a histogram of try/catch-block counts over every method whose descriptor ends in )Z. The five handlers stand far apart from everything else, so if exactly five methods share one large even count, the geometry is intact and only the number changed. |
Matching a fingerprint proves structure, not meaning. It cannot detect a revision that keeps every id's length but reassigns what those ids mean, so choosing a profile is an acknowledgement rather than a proof.
The files under data/ were produced by the packet-analysis module and are
checked in, so normal use needs nothing further. The extraction code is the
library in packet-analysis/src/main: it reads the packet table out of the
bytecode, recovers each payload's field layout, and maps packets from one
revision onto the next. There is no command that regenerates data/ — the
generators that drove the library are not part of the repository — so adding a
revision means calling that library yourself.
One step is easy to get wrong: deciding which RuneLite builds actually carry a given revision's packet table. Do not group builds by how many entries their packet table has. Several builds carry a 101-entry or 117-entry client table while assigning entirely different ids. Builds 1.11.24 through 1.12.14 have revision 236's table size, and 1.12.25 through 1.12.29 have revision 238's, and neither is that revision. Of 103 published builds, only 30 match one of the six tables here. Compare every id-to-length pair instead, and use only builds that match exactly.
This is not a small effect. Standing in size-matched but id-mismatched builds for revisions 236 and 238 measured cross-revision accuracy at 7.7%; using the aligned builds and changing nothing else took the same measurement to 40.3%.
A launcher built on the injected client has to reach some fields by reflection,
because RuneLite's API does not expose them: the selected scene tile, an actor's
path length, the widget component table, the login statics. Their obfuscated
names change every revision, and one obfuscated name can mean two different
things two revisions apart — at 1.12.33 dj is TileItem, at 1.12.35 it is
PacketWriter. Carrying a name forward is how you point a hook at the wrong
class.
./gradlew :packet-analysis:shadedJar
java -jar packet-analysis/build/libs/rl-suite-hooks-1.3.1-all.jar --help| Mode | What it does |
|---|---|
--derive --jar NEW.jar |
Locates each hook in a client JAR. |
--verify --jar JAR --mappings M.json |
Checks every class, field and method a mapping file names exists in that JAR with the declared descriptor. Exits 1 if any is missing. |
--buffer-infra --jar JAR |
Prints the packet buffer and writer infrastructure: the buffer class and its offset multipliers, ClientPacket, PacketBufferNode, PacketWriter and its ISAAC field. |
--extract-semantic-seed --jar MAPPED --raw-jar RAW --tsv OUT |
Transcribes Devious-style mapping annotations and binds them to the exact raw JAR checksum. |
--update-semantic --old-jar OLD --new-jar NEW --old-map MAP --tsv OUT |
Carries semantic names to a new revision using reciprocal structural matches. Any unresolved entry prevents output. |
--import-version-package --jar JAR --mappings version-package.json --source-commit COMMIT --tsv OUT |
Validates a runtime mapping package against the exact JAR and converts it to native semantic TSV. |
--merge-semantic --jar JAR --maps A.tsv,B.tsv --tsv OUT |
Combines checksum-identical semantic maps, rejecting physical or semantic conflicts. |
Every rule is anchored on something the injector cannot rename — an interface a
class declares, or an injected public getter — and reads the answer out of the
bytecode. That makes the rules revision-independent by construction, which is a
claim about them, not a proof. So --derive takes a control:
java -jar packet-analysis/build/libs/rl-suite-hooks-1.3.1-all.jar --derive \
--control injected-client-1.12.33.jar \
--control-hooks hooks-1.12.33.json \
--jar injected-client-1.12.35.jarEach rule must first reproduce the known-good answer on the older JAR. If any disagrees, nothing is printed for the new one:
control: 21/21 known hooks reproduced from injected-client-1.12.33.jar
derived from injected-client-1.12.35.jar:
Actor.pathLength dh.bb:I *-1798356091
Scene.selectedX ez.bj:I
...
The control file is any JSON with classMappings, staticFields and garbage
in the shape the mapping tools already use. A rule that has quietly stopped
matching fails loudly instead of returning a plausible wrong name — which is
what caught a bug in the scene-selection rule while it was being written.
--derive finds names, not meanings. That a field is called pathLength is
still a claim to check against a running client.
The checksum-bound maps at data/semantic/1.12.11.tsv through
data/semantic/1.12.14.tsv are reproduced directly from each annotated Devious
client and matching official injected client. 1.12.14.1.tsv is the exact
structural continuation, and rev-235.tsv is the original 1.12.11 seed retained
under its game revision:
java -jar packet-analysis/build/libs/rl-suite-hooks-1.3.1-all.jar \
--extract-semantic-seed --jar mapped-235.jar --raw-jar injected-client-1.12.11.jar \
--tsv semantic-235.tsv --revision 235 --source-commit COMMIT
java -jar packet-analysis/build/libs/rl-suite-hooks-1.3.1-all.jar \
--update-semantic --old-jar injected-client-1.12.11.jar --new-jar injected-client-next.jar \
--old-map semantic-235.tsv --tsv semantic-next.tsv --revision NEXTThe updater is intentionally fail-closed. A revision that changes or removes a semantic class still needs an explicit reviewed transition; similarity alone is not permission to publish a partial map.
The known transitions from 1.12.11 through 1.12.14.1 reproduce all 4,225 or 4,226 identities with zero false or missing rows. The true 1.12.14.1→1.12.15 reobfuscation reset remains intentionally unpublished until every unresolved identity has reviewed evidence.
data/semantic/anchors/ contains the smaller checksum-bound set that has passed
that stricter bar: 136 identities at 1.12.15 and 113 identities carried through
the current 1.12.37 snapshot by the structural updater. The current map merges
those with 68 additional non-conflicting identities propagated from an external
runtime mapping package, for 181 total. These are valid semantic maps for analysis, but they are deliberately
not presented as complete replacements for the 4,226-row pre-reset map.
Reviewed exceptional transitions live in data/semantic/overrides/.
The bridge is data-driven; rl-suite does not copy or run the source mapper. It validates a generated runtime package against its exact source JAR, converts it to native TSV, advances it with the existing fail-closed updater, then merges it with independently reviewed target anchors:
HOOKS=packet-analysis/build/libs/rl-suite-hooks-1.3.1-all.jar
SOURCE_JAR=/path/to/injected-client-1.12.35.jar
TARGET_JAR=/path/to/injected-client-1.12.37-20260819.170454-4.jar
java -jar "$HOOKS" --import-version-package \
--jar "$SOURCE_JAR" --mappings version-package.json \
--source-commit 4cea48fc258486ddb2e3c1291be6a93b7c1052f2 \
--tsv data/semantic/anchors/1.12.35-version-package.tsv
java -jar "$HOOKS" --update-semantic \
--old-jar "$SOURCE_JAR" --new-jar "$TARGET_JAR" \
--old-map data/semantic/anchors/1.12.35-version-package.tsv \
--anchors data/semantic/anchors/1.12.37-SNAPSHOT-reviewed.tsv \
--overrides data/semantic/overrides/1.12.35-version-package-to-1.12.36.tsv \
--tsv data/semantic/anchors/1.12.37-SNAPSHOT-version-package.tsv \
--revision 1.12.37-20260819.170454-4
java -jar "$HOOKS" --merge-semantic --jar "$TARGET_JAR" \
--maps data/semantic/anchors/1.12.37-SNAPSHOT-reviewed.tsv,data/semantic/anchors/1.12.37-SNAPSHOT-version-package.tsv \
--tsv data/semantic/anchors/1.12.37-SNAPSHOT.tsvFour RuneLite-injected fields cross revisions through their stable scalar
@Named annotations. The two explicit drops are fields removed in 1.12.36;
all other uncertainty still prevents output. The imported mapping data remains
subject to the BSD 2-Clause terms reproduced below.
--overrides accepts eight tab-separated columns with comments beginning #:
kind, old owner/name/descriptor, map or drop, and new owner/name/descriptor.
Class identities use - for member name and descriptor. A drop uses - in all
three target columns. Every override must name an existing old semantic entry;
every mapped target must exist in the new JAR.
--anchors TARGET.tsv accepts a partial semantic map for the new JAR. Its
input.sha256 must match that JAR. Unique semantic identities shared with the
old map become reviewed transitions before structural propagation. Member
identities include the semantic owner class, so common names in unrelated
classes do not collide. Equally ambiguous overload groups remain structural
matches rather than guessed; asymmetric ambiguity and conflicts with
--overrides are rejected.
class<TAB>ea<TAB>-<TAB>-<TAB>drop<TAB>-<TAB>-<TAB>-
field<TAB>og<TAB>ar<TAB>Lea;<TAB>map<TAB>NEW_OWNER<TAB>NEW_NAME<TAB>LNEW_TYPE;
- It is not a general OSRS gamepack deobfuscator. It expects RuneLite's already injected client.
- Without a reviewed semantic map it does not infer meanings.
field33is stable and traceable, not descriptive. - It normalizes only arithmetic multipliers proven safe across every access to a field. It does not guess through irregular arithmetic, control-flow flattening, or string encryption.
- Passing bytecode verification proves the output is structurally valid. It does not prove it behaves identically to the input.
Six passes, in order. Each finishes its analysis before anything is modified, and the result is verified twice before it is written.
| Pass | What it does |
|---|---|
strip-malformed-named |
Removes the invisible array-valued javax.inject.Named annotations that break decompilers, keeping the four legitimate ones. |
normalize-packet-handlers |
Collapses each packet reader's fragmented try/catch entries down to two and flattens the guarded return gaps. |
normalize-condy-concats |
Rewrites three nested constant-dynamic string concatenations as ordinary bytecode. This is why the output is analysis-only. |
normalize-proven-decoders |
Removes numeric field multipliers only when every read and write proves the same reversible encoding. |
mark-unused-members |
Marks members nothing references with @ToRemove. It never deletes anything. |
rename-symbols |
Gives every class, field and method a deterministic name, and records the original in a RuneLite mapping annotation. |
Two modules: transform-jar holds the pipeline and CLI, packet-analysis holds
the packet extraction and cross-revision mapping.
The imported mapping rows under data/semantic/ are derived from
Solace mapping commit
4cea48fc258486ddb2e3c1291be6a93b7c1052f2 and retain its BSD 2-Clause terms:
BSD 2-Clause License
Copyright (c) 2026, Solace contributors
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.