package javastory.io;
import java.awt.Point;
import java.nio.charset.Charset;
import java.util.List;
import javastory.tools.FiletimeUtil;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
/**
* Provides methods to construct a game packet.
*
* This class uses an internal LinkedList structure to auto-expand the buffer.
* This makes it pro as hell.
*
* @author shoftee
*/
public class PacketBuilder {
private static final Charset ASCII = Charset.forName("US-ASCII");
private final List<byte[]> buffers;
private byte[] currentBuffer;
private int currentCapacity;
private int nextCapacity;
private int currentPosition;
private int globalPosition;
/**
* Class constructor.
*
* The initial capacity is set to 32.
*/
public PacketBuilder() {
this(32);
}
/**
* Class constructor.
*
* @param initialCapacity
* the initial capacity for the packet buffer.
*
* @throws IllegalArgumentException
* if <code>initialCapacity</code> is non-positive.
*/
public PacketBuilder(final int initialCapacity) {
Preconditions.checkArgument(initialCapacity > 0);
this.buffers = Lists.newLinkedList();
this.currentCapacity = initialCapacity;
this.nextCapacity = initialCapacity;
this.currentBuffer = new byte[initialCapacity];
this.buffers.add(this.currentBuffer);
}
/**
* Allocates a new byte buffer at the end of the buffers list. The new
* buffer has twice the capacity as the current.
*
* The method makes the new buffer the current one.
*/
private void allocateNext() {
this.currentBuffer = new byte[this.currentCapacity];
this.currentPosition = 0;
this.buffers.add(this.currentBuffer);
this.currentCapacity = this.nextCapacity;
this.nextCapacity *= 2;
}
/**
* Writes a single byte to the current buffer, or to a newly-allocated one
* if the current is full.
*
* @param b
* the byte to write.
*/
private void writeByteInternal(final byte b) {
if (this.currentPosition == this.currentCapacity) {
this.allocateNext();
}
this.currentPosition++;
this.globalPosition++;
this.currentBuffer[this.currentPosition] = b;
}
/**
* Writes a byte array.
*
* @param bytes
* the byte array to write.
*/
private void writeArrayInternal(final byte[] bytes) {
int written = 0;
int remaining = bytes.length;
while (true) {
final int free = this.currentCapacity - this.currentPosition;
if (free != 0) {
// We write as much as we can.
// current position in source : written
// current position in destination: this.currentPosition
// bytes to write: min(remaining, free)
final int payload = Math.min(free, remaining);
System.arraycopy(bytes, written, this.currentBuffer, this.currentPosition, payload);
written += payload;
remaining -= payload;
}
if (remaining == 0) {
return;
}
this.allocateNext();
}
}
/**
* Writes a number in reverse byte order.
*
* @param number
* @param byteCount
*/
private void writeReverse(long number, final int byteCount) {
for (int i = 0; i < byteCount; i++) {
final byte b = (byte) (number & 0xFF);
this.writeByteInternal(b);
number >>>= 8;
}
}
public GamePacket getPacket() {
final byte[] total = new byte[this.globalPosition];
int index = 0;
// We take all the buffers except the current one.
for (int i = 0; i < this.buffers.size() - 1; i++) {
// Get the i-th buffer.
final byte[] buffer = this.buffers.get(i);
// copy its contents to the big array.
System.arraycopy(buffer, 0, total, index, buffer.length);
index += buffer.length;
}
// Finally copy the current buffer separately (it may be incomplete)
System.arraycopy(this.currentBuffer, 0, total, index, this.currentPosition);
return GamePacket.wrapperOf(total);
}
/**
* Writes a specified number of null (0x00) bytes.
*
* @param count
* the number of null bytes to write.
*/
public void writeZeroBytes(final int count) {
for (int i = 0; i < count; i++) {
this.writeByteInternal((byte) 0);
}
}
/**
* Writes an array.
*
* @param bytes
* the array to write.
*/
public void writeBytes(final byte[] bytes) {
for (final byte b : bytes) {
this.writeByteInternal(b);
}
}
/**
* Writes a byte.
*
* @param b
* the byte to write.
*/
public void writeByte(final byte b) {
this.writeByteInternal(b);
}
/**
* Writes a byte with the value 1 if <code>bool</code> is <code>true</code>
* or 0 if it's <code>false</code>.
*
* @param bool
* The boolean value to write.
*/
public void writeAsByte(final boolean bool) {
this.writeAsByte(bool ? 1 : 0);
}
/**
* Writes a 16-bit integer with the value 1 if <code>bool</code> is
* <code>true</code> or 0 if it's <code>false</code>.
*
* @param bool
* The boolean value to write.
*/
public void writeAsShort(final boolean bool) {
this.writeAsShort(bool ? 1 : 0);
}
/**
* Writes a number as a byte. The major bits will be truncated.
*
* @param number
* the number to write.
*/
public void writeAsByte(final int number) {
this.writeByteInternal((byte) number);
}
/**
* Writes a number as a 16-bit integer. The major bits will be truncated.
*
* @param number
* the number to write.
*/
public void writeAsShort(final int number) {
this.writeReverse(number, 2);
}
/**
* Writes a number as a 32-bit integer.
*
* @param number
* the number to write.
*/
public void writeInt(final int number) {
this.writeReverse(number, 4);
}
/**
* Writes a 64-bit integer.
*
* @param number
* the number to write.
*/
public void writeLong(final long number) {
this.writeReverse(number, 8);
}
/**
* Writes a simple string.
*
* @param string
* the string to write.
*/
private void writeString(final String string) {
this.writeArrayInternal(string.getBytes(ASCII));
}
/**
* Writes a string padded to a specified length.
*
* @param string
* the string to write.
* @param totalLength
* the length to pad to.
*
* @throws IllegalArgumentException
* if the length of the given string is greater than
* <code>totalLength</code>.
*/
public void writePaddedString(final String string, final int totalLength) {
final int length = string.length();
Preconditions.checkArgument(length <= totalLength);
this.writeString(string);
for (int i = length; i < totalLength; i++) {
this.writeAsByte(0);
}
}
/**
* Writes a length-prefixed string.
*
* @param string
* the string to write.
*/
public void writeLengthPrefixedString(final String string) {
this.writeAsShort((short) string.length());
this.writeString(string);
}
/**
* Writes a null-terminated string.
*
* @param string
* the string to write.
*/
public void writeNullTerminatedString(final String string) {
this.writeString(string);
this.writeAsByte(0);
}
/**
* Writes a two-dimensional point, represented as two 16-bit integers.
*
* @param point
* the point to write.
*/
public void writeVector(final Point point) {
this.writeAsShort((short) point.x);
this.writeAsShort((short) point.y);
}
public void writeAsFiletime(final long unixtime) {
this.writeLong(FiletimeUtil.getFiletime(unixtime));
}
}