377 Protocol

From RuneWiki

The 377 Protocol does not differ substantially from that of previous versions of RS2. There are a small number of notable differences. These are the minor variations to the order bits are sent during player updating, sending the appearance update block in reverse order and most of the opcode numbers being different to 317 (due to the fact most of them are changed every release).

There are new opcodes that were not present in the 317 protocol, such as the ability to display full screen interfaces.

This page will detail the variations in the protocol along with any adjustments to the packet numbers or lengths. Not all of the packet numbers are different from 317. For example the packets used during login (the login, connection and reconnection packet numbers) are exactly the same as they are in 317.

An opcode is an 'operation code' used to identify which packet is to be sent or received. It is sometimes called a message or commonly a frame in Winterlove. The opcode is followed by the packet size. The packet size can be fixed or varied. Varied sizes exist in cases where the packet length is known on running, for example the length of the chat packet can vary as the number of bytes needed to be sent is not fixed (we can send one character or a great number of them). The opcode size is said to be 'variable short' if the size is variable but exceeds that of a single byte (2 ^ 8 or 256 values 0-255), this can happen during player and npc updating. Finally the opcode and opcode size is followed by the payload, the payload is the actual contents of the packet we are sending consisting of N number of bytes.

The client uses -1 and -2 to specify that an opcode is not of fixed length with the number representing the amount of bytes, e.g -1 = 1 byte, -2 = 2 bytes

How do we get an opcode? (The protocol decoder)

The following is an example of a functional protocol decoder along with an explanation of what it is doing:

 1 package com.runescape.net.codec;
 3 import com.runescape.net.packet.Packet;
 4 import com.runescape.util.ISAAC;
 6 import org.apache.mina.core.buffer.IoBuffer;
 7 import org.apache.mina.core.session.IoSession;
 9 import org.apache.mina.filter.codec.CumulativeProtocolDecoder;
10 import org.apache.mina.filter.codec.ProtocolDecoderOutput;
12 /**
13 * The game protocol decoder
14 */
15 public class RSDecoder extends CumulativeProtocolDecoder
16 {
18 	/**
19 	* Decodes the game protocol
20 	* An opcode is read with a single byte which is then encrypted to prevent packet injection
21 	* The size of the opcode is then determined, if it is not fixed we read another byte
22 	* Finally we get the payload
23 	*
24 	* @param session The session
25 	* @param in The buffer
26 	* @param out The protocol decoder output
27 	* @throws Exceptions encountered during decoding
28 	* @return Used to check if we have finished reading
29 	*/
30 	@Override
31 	protected boolean doDecode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception
32 	{
33 		if(in.remaining() >= 1) 
34 		{
35 			int opcode = in.get() & 0xff;
36 			ISAAC cipher = (ISAAC) session.getAttribute("decryption");
37 			if(cipher != null)
38 				opcode = (opcode - cipher.generateNextKey()) & 0xff;
39 			int size = Packet.PACKET_SIZES[opcode];
40 			if(size == -1) 
41 			{
42 				if(in.remaining() >= 1) 
43 					size = in.get() & 0xff;
44 				else 
45 				{
46 					in.rewind();
47 					return false;
48 				}
49 			}
50 			if(in.remaining() < size)
51 				return true;
53 			byte[] data = new byte[size];
54 			in.get(data);
55 			System.out.println("Reading opcode " + opcode + " size " + size);
56 			return false;
57 		} else 
58 			in.rewind();
60 		return true;
61 	}
62 }

As you can see the method is actually quite simple.

We read a byte from the client which is the opcode. We then use a key obtained during the login session to decrypt the encrypted opcode. We then check the size of the opcode to determine if the length is fixed or not, if it is not we read another byte to determine the correct size. You may have noticed there is no check to see if the opcode is of variable short length here, that is because in 377 there is no need for the server to send an opcode of variable short length. The client implementation however does need one because the server sends the player and npc updating packets which can be larger than a byte. Finally we read the payload and tell the server to print the opcode and size to standard output.

How do we send an opcode? (The protocol encoder)

The following is an example of a functional protocol encoder and an explanation of what it is doing:

 1 package com.runescape.net.codec;
 3 import com.runescape.net.packet.Packet;
 4 import static com.runescape.net.packet.Packet.Type;
 5 import com.runescape.util.ISAAC;
 7 import org.apache.mina.core.buffer.IoBuffer;
 9 import org.apache.mina.core.session.IoSession;
11 import org.apache.mina.filter.codec.ProtocolEncoder;
12 import org.apache.mina.filter.codec.ProtocolEncoderOutput;
14 /**
15 * The game protocol encoder
16 */
17 public class RSEncoder implements ProtocolEncoder
18 {
20 	/**
21 	* Encodes the game protocol
22 	* Writes an opcode, then write the length of the opcode and then write the payload
23 	* If the length of the opcode is not fixed it is either variable (+1, 1 bytes) or variable short (+2, 2 bytes)
24 	*
25 	* @param session The session
26 	* @param message The object to be sent as a message
27 	* @param out The protocol encoder output
28 	* @throws Any exceptions during encoding
29 	*/
30 	@Override
31 	public void encode(IoSession session, Object message, ProtocolEncoderOutput out) throws Exception
32 	{
33 		Packet packet = (Packet) message;
34 		int opcode = packet.getOpcode();
35 		ISAAC cipher = (ISAAC) session.getAttribute("encryption");
36 		opcode += cipher.generateNextKey();
37 		int size = packet.getSize();
38 		int length = size + 1;
39 		Type type = packet.getType();
40 		switch(type)
41 		{
42 			case VARIABLE:
43 				length += 1;
44 				break;
46 			case VARIABLE_SHORT:
47 				length += 2;
48 				break;
49 		}
50 		IoBuffer buffer = IoBuffer.allocate(length);
51 		buffer.put((byte) opcode);
52 		switch(type)
53 		{
54 			case VARIABLE:
55 				buffer.put((byte) size);
56 				break;
58 			case VARIABLE_SHORT:
59 				buffer.putShort((short) size);
60 				break;
61 		}
62 		buffer.put(packet.getPayload());
63 		out.write(buffer.flip());
64 	}
66 	/**
67 	* Releases resources used by this encoder
68 	* @param session The session
69 	*/
70 	@Override
71 	public void dispose(IoSession session)
72 	{
74 	}
75 }

We start by constructing a Packet instance containing information about the opcode and the opcode size. The opcode is encrypted using the session key obtained during login. The server already knows what the opcode is unlike in the decoding stage. We need to adjust the opcode to the correct size, the size is either fixed, variable or variable short. The process is the reverse of the encoding stage, instead of reading a -1 or -2 type we are sending the length to the client to be read.

The difference between 'size', 'length' and 'type' here may be confusing. The size is the actual size of the payload, the length is the size of the payload adjusted to include the header (+ 1 or + 2), the type is whether the opcode is fixed, variable or variable short which has nothing to do with actual size, it just tells us what size we need to adjust the packet to.

The opcode is written as a byte, followed by the length (written as either a byte or short) and then the payload, we construct a buffer with the length of the opcode so the correct amount of data is sent.

What opcodes exist in 377?

How does the player updating differ from previous versions?