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<Short, ServerInformation> get() throws IOException { Map<Short, ServerInformation> 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<Short, ServerInformation>) 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<Short, ServerInformation>(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<String> locations = new ArrayList<String>(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 <tt>DataInputStream</tt>. * <p> * Jagex decodes its variable-length integers likely to fall within the range <tt>0..127</tt> very often using this algorithm: * <ol> * <li>Read one byte. * <li>If the byte is under <code>0x80</code>, return that byte as is, otherwise: * <ol> * <li>Unset bit 7 of the byte. * <li>Shift it to the left so its bit 6 becomes bit 14. * <li>Read another byte and bitwise-or the result of the shift and the second byte. * </ol> * </ol> * * @param in * The <tt>DataInputStream</tt> to decode a <code>short</code> from using the above algorithm. * @return the shortened integer read from the given <tt>DataInputStream</tt> * @throws BufferUnderflowException * if an error occurs while reading any of the bytes needed to decode a shortened integer from the given <tt>DataInputStream</tt> */ 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 <tt>DataInputStream</tt>. * <p> * Jagex decodes its strings using this algorithm: * <ol> * <li>Read one byte. It must be <code>0x00</code>. * <li>Read one byte, call it <code>last</code>. * <li>While <code>last</code> is not <code>0x00</code>, add <code>last</code> to the string and read another byte into <code>last</code>. * </ol> * * @param in * The <tt>DataInputStream</tt> to decode a <tt>String</tt> from using the above algorithm. * @return the string read from the given <tt>DataInputStream</tt> * @throws BufferUnderflowException * if an error occurs while reading any of the bytes needed to decode a string from the given <tt>DataInputStream</tt> * @throws IllegalArgumentException * if the first byte read is not <code>0x00</code> */ 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(); }}