All pastes #1229849 Raw Edit

RuneScape server info

public java v1 · immutable
#1229849 ·published 2008-10-18 01:24 UTC
rendered paste body
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();	}}