package tests.runescape; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.Socket; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; public final class ServerInfoGetter { private static final byte[] serverInformationRequest = new byte[] { 0x66, 0x00, 0x00, 0x00, 0x00 }; private static final byte[] playerCountRequest = new byte[] { 0x66, 0x62, 0x6C, 0x4F, (byte) 0xA7 }; public static Map get() throws IOException { Map result = null; boolean readFromDisk = false; // If we have a server information packet on disk from the last hour, // use it. We'll have to get player counts anyway. final File file = new File(System.getProperty("user.dir"), "runescape-server-information.jo"); if (file.exists() && file.lastModified() + 60 * 60 * 1000 > new Date().getTime()) { try { final ObjectInputStream disk = new ObjectInputStream(new FileInputStream(file)); result = (Map) disk.readObject(); readFromDisk = true; } catch (final FileNotFoundException e) { // don't mind this exception; we'll get a packet from Jagex } catch (final IOException e) { // as above } catch (final ClassNotFoundException e) { // don't mind this exception either, though it's a bit more // serious } } if (!readFromDisk) { // 1. Connect to game server 1, port 43594. If this does not succeed, // an IOException will be thrown. final Socket s = new Socket(); s.connect(new InetSocketAddress("world1.runescape.com", 43594), 5000); try { final OutputStream out = s.getOutputStream(); final DataInputStream in = new DataInputStream(new BufferedInputStream(s.getInputStream(), 512)); // 2. Get server activities, locations and other information. // This does not deal with the number of players on the server. result = new HashMap(128); // a) Send the request for server information. out.write(serverInformationRequest); // b) Get the reply from the game server. // First we will get the length of the packet, as a 2-byte integer. final short replyLength = (short) (in.readShort() - 4); // ^ Exclude the 4 bytes for the length and 2 unknown bytes after. in.readShort(); // Dummy read for unknown bytes 01 01 // And we get a ByteBuffer with the whole reply for the rest. ByteBuffer data; { final byte[] b = new byte[replyLength]; in.readFully(b); data = ByteBuffer.wrap(b); } // c) The reply is in two parts. The first part has a certain // number of flag ID (which we will skip) and location pairs. final short numLocations = readShortenedInt(data); final List locations = new ArrayList(numLocations); for (int i = 0; i < numLocations; i++) { // Skip the flag ID. readShortenedInt(data); // Get the location. locations.add(readString(data)); } // d) The second part has a list of servers along with an // association with a location, a series of boolean values // to indicate features found on the server, and an activity // recommendation or mandate from Jagex. // Skip one byte. It's always 01. data.get(); // Skip the highest server number. data.getShort(); final short servers = readShortenedInt(data); for (int i = 0; i < servers; i++) { final short number = (short) (readShortenedInt(data) + 1); final String location = locations.get(readShortenedInt(data)); // Skip 3 bytes which are always 0. for (int j = 0; j < 3; j++) data.get(); // The next byte is a bitfield, most significant bit first: // 000A LQPM // A = Activity mandatory; L = Lootshare; // Q = Quick chat only; P = Pvp; M = Members only boolean activityMandatory, lootShare, quickChatOnly, playerVsPlayer, membersOnly; { final byte bitfield = data.get(); activityMandatory = (bitfield & 0x10) == 0x10; lootShare = (bitfield & 0x08) == 0x08; quickChatOnly = (bitfield & 0x04) == 0x04; playerVsPlayer = (bitfield & 0x02) == 0x02; membersOnly = (bitfield & 0x01) == 0x01; } String activity = readString(data); if (activity.equals("-")) activity = null; final String hostname = readString(data); result.put(number, new ServerInformation(location, activity, activityMandatory, lootShare, quickChatOnly, playerVsPlayer, membersOnly, hostname)); } try { final ObjectOutputStream disk = new ObjectOutputStream(new FileOutputStream(file)); disk.writeObject(result); } catch (final FileNotFoundException e) { // don't mind this exception } catch (final IOException e) { // as above } } finally { s.close(); } } // 1. Connect to game server 1, port 43594. If this does not succeed, // an IOException will be thrown. final Socket s = new Socket(); s.connect(new InetSocketAddress("world1.runescape.com", 43594), 5000); try { final OutputStream out = s.getOutputStream(); final DataInputStream in = new DataInputStream(new BufferedInputStream(s.getInputStream(), 512)); // 3. Get player counts. // a) Send the request for player counts. out.write(playerCountRequest); // b) Get the reply from the game server. // First we will get the length of the packet, as a 2-byte integer. final short replyLength = (short) (in.readShort() - 4); // ^ Exclude the 4 bytes for the length and 2 unknown bytes after. in.readShort(); // Dummy read for unknown bytes 01 00 // And we get a ByteBuffer with the whole reply for the rest. ByteBuffer data; { final byte[] b = new byte[replyLength]; in.readFully(b); data = ByteBuffer.wrap(b); } // c) The reply is a series of encoded server number and // player count pairs. The player count is an unshortened 16-bit // value. for (int i = 0; i < result.size(); i++) { final short number = (short) (readShortenedInt(data) + 1); final short players = data.getShort(); final ServerStatus status = (players >= 1980 ? ServerStatus.FULL : (players == 0 ? ServerStatus.OFFLINE : ServerStatus.ONLINE)); result.put(number, new ServerInformation(result.get(number), players, status)); } } catch (BufferUnderflowException bue) { throw new IOException(bue); } catch (IllegalArgumentException iae) { throw new IOException(iae); } finally { s.close(); } // 4. We're done! return result; } /** * Reads a shortened integer from the given DataInputStream. *

* Jagex decodes its variable-length integers likely to fall within the range 0..127 very often using this algorithm: *

    *
  1. Read one byte. *
  2. If the byte is under 0x80, return that byte as is, otherwise: *
      *
    1. Unset bit 7 of the byte. *
    2. Shift it to the left so its bit 6 becomes bit 14. *
    3. Read another byte and bitwise-or the result of the shift and the second byte. *
    *
* * @param in * The DataInputStream to decode a short from using the above algorithm. * @return the shortened integer read from the given DataInputStream * @throws BufferUnderflowException * if an error occurs while reading any of the bytes needed to decode a shortened integer from the given DataInputStream */ private static short readShortenedInt(final ByteBuffer data) throws BufferUnderflowException { short result = (short) (data.get() & 0xFF); if (result >= 0x80) result = (short) (((result & 0x7F) << 8) | (data.get() & 0xFF)); return result; } /** * Reads a string from the given DataInputStream. *

* Jagex decodes its strings using this algorithm: *

    *
  1. Read one byte. It must be 0x00. *
  2. Read one byte, call it last. *
  3. While last is not 0x00, add last to the string and read another byte into last. *
* * @param in * The DataInputStream to decode a String from using the above algorithm. * @return the string read from the given DataInputStream * @throws BufferUnderflowException * if an error occurs while reading any of the bytes needed to decode a string from the given DataInputStream * @throws IllegalArgumentException * if the first byte read is not 0x00 */ private static String readString(final ByteBuffer data) throws BufferUnderflowException { if (data.get() != 0x00) throw new IllegalArgumentException("0x00 expected at the start of a Jagex-encoded string"); final ByteArrayOutputStream temp = new ByteArrayOutputStream(); byte last; while ((last = data.get()) != 0x00) temp.write(last); final Charset utf8 = Charset.forName("utf-8"); final ByteBuffer in = ByteBuffer.wrap(temp.toByteArray()); return utf8.decode(in).toString(); } }