diff --git a/pom.xml b/pom.xml
index 488df03163..e95d8b9b3c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -61,6 +61,11 @@
netty-codec
4.1.65.Final
+
+ io.netty
+ netty-buffer
+ 4.1.65.Final
+
@@ -96,6 +101,20 @@
js-scriptengine
${graalvm.version}
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ 5.7.2
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.7.2
+
+
+
diff --git a/src/main/java/net/packet/ByteBufInPacket.java b/src/main/java/net/packet/ByteBufInPacket.java
new file mode 100644
index 0000000000..0977172354
--- /dev/null
+++ b/src/main/java/net/packet/ByteBufInPacket.java
@@ -0,0 +1,81 @@
+package net.packet;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+
+import java.awt.*;
+
+public class ByteBufInPacket implements InPacket {
+ private final ByteBuf byteBuf;
+
+ public ByteBufInPacket(ByteBuf byteBuf) {
+ this.byteBuf = byteBuf;
+ }
+
+ @Override
+ public byte[] getBytes() {
+ return ByteBufUtil.getBytes(byteBuf);
+ }
+
+ @Override
+ public byte readByte() {
+ return byteBuf.readByte();
+ }
+
+ @Override
+ public short readShort() {
+ return byteBuf.readShortLE();
+ }
+
+ @Override
+ public int readInt() {
+ return byteBuf.readIntLE();
+ }
+
+ @Override
+ public long readLong() {
+ return byteBuf.readLongLE();
+ }
+
+ @Override
+ public Point readPoint() {
+ final short x = byteBuf.readShortLE();
+ final short y = byteBuf.readShortLE();
+ return new Point(x, y);
+ }
+
+ @Override
+ public String readString() {
+ short length = readShort();
+ byte[] stringBytes = new byte[length];
+ byteBuf.readBytes(stringBytes);
+ return new String(stringBytes, STRING_CHARSET);
+ }
+
+ @Override
+ public byte[] readBytes(int numberOfBytes) {
+ byte[] bytes = new byte[numberOfBytes];
+ byteBuf.readBytes(bytes);
+ return bytes;
+ }
+
+ @Override
+ public void skip(int numberOfBytes) {
+ byteBuf.skipBytes(numberOfBytes);
+ }
+
+ @Override
+ public int available() {
+ return byteBuf.readableBytes();
+ }
+
+ @Override
+ public void seek(int byteOffset) {
+ byteBuf.readerIndex(byteOffset);
+ }
+
+ @Override
+ public int getPosition() {
+ return byteBuf.readerIndex();
+ }
+}
diff --git a/src/main/java/net/packet/ByteBufOutPacket.java b/src/main/java/net/packet/ByteBufOutPacket.java
new file mode 100644
index 0000000000..5aa7a20b79
--- /dev/null
+++ b/src/main/java/net/packet/ByteBufOutPacket.java
@@ -0,0 +1,83 @@
+package net.packet;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import net.jcip.annotations.NotThreadSafe;
+import net.opcodes.SendOpcode;
+
+import java.awt.*;
+
+@NotThreadSafe
+public class ByteBufOutPacket implements OutPacket {
+ private final ByteBuf byteBuf;
+
+ public ByteBufOutPacket(SendOpcode op) {
+ ByteBuf byteBuf = Unpooled.buffer();
+ byteBuf.writeShortLE((short) op.getValue());
+ this.byteBuf = byteBuf;
+ }
+
+ public ByteBufOutPacket(SendOpcode op, int initialCapacity) {
+ ByteBuf byteBuf = Unpooled.buffer(initialCapacity);
+ byteBuf.writeShortLE((short) op.getValue());
+ this.byteBuf = byteBuf;
+ }
+
+ @Override
+ public byte[] getBytes() {
+ return ByteBufUtil.getBytes(byteBuf);
+ }
+
+ @Override
+ public void writeByte(byte value) {
+ byteBuf.writeByte(value);
+ }
+
+ @Override
+ public void writeByte(int value) {
+ writeByte((byte) value);
+ }
+
+ @Override
+ public void writeBytes(byte[] value) {
+ byteBuf.writeBytes(value);
+ }
+
+ @Override
+ public void writeShort(int value) {
+ byteBuf.writeShortLE(value);
+ }
+
+ @Override
+ public void writeInt(int value) {
+ byteBuf.writeIntLE(value);
+ }
+
+ @Override
+ public void writeLong(long value) {
+ byteBuf.writeLongLE(value);
+ }
+
+ @Override
+ public void writeBoolean(boolean value) {
+ byteBuf.writeByte(value ? 1 : 0);
+ }
+
+ @Override
+ public void writeString(String value) {
+ writeShort((short) value.length());
+ writeBytes(value.getBytes(STRING_CHARSET));
+ }
+
+ @Override
+ public void writePoint(Point value) {
+ writeShort((short) value.getX());
+ writeShort((short) value.getY());
+ }
+
+ @Override
+ public void skip(int numberOfBytes) {
+ writeBytes(new byte[numberOfBytes]);
+ }
+}
diff --git a/src/main/java/net/packet/InPacket.java b/src/main/java/net/packet/InPacket.java
new file mode 100644
index 0000000000..2c899c2ade
--- /dev/null
+++ b/src/main/java/net/packet/InPacket.java
@@ -0,0 +1,17 @@
+package net.packet;
+
+import java.awt.*;
+
+public interface InPacket extends Packet {
+ byte readByte();
+ short readShort();
+ int readInt();
+ long readLong();
+ Point readPoint();
+ String readString();
+ byte[] readBytes(int numberOfBytes);
+ void skip(int numberOfBytes);
+ int available();
+ void seek(int byteOffset);
+ int getPosition();
+}
diff --git a/src/main/java/net/packet/OutPacket.java b/src/main/java/net/packet/OutPacket.java
new file mode 100644
index 0000000000..57e953a6f7
--- /dev/null
+++ b/src/main/java/net/packet/OutPacket.java
@@ -0,0 +1,16 @@
+package net.packet;
+
+import java.awt.*;
+
+public interface OutPacket extends Packet {
+ void writeByte(byte value);
+ void writeByte(int value);
+ void writeBytes(byte[] value);
+ void writeShort(int value);
+ void writeInt(int value);
+ void writeLong(long value);
+ void writeBoolean(boolean value);
+ void writeString(String value);
+ void writePoint(Point value);
+ void skip(int numberOfBytes);
+}
diff --git a/src/main/java/net/packet/Packet.java b/src/main/java/net/packet/Packet.java
new file mode 100644
index 0000000000..79f257cd38
--- /dev/null
+++ b/src/main/java/net/packet/Packet.java
@@ -0,0 +1,10 @@
+package net.packet;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+public interface Packet {
+ Charset STRING_CHARSET = StandardCharsets.US_ASCII;
+
+ byte[] getBytes();
+}
diff --git a/src/test/java/net/packet/ByteBufInPacketTest.java b/src/test/java/net/packet/ByteBufInPacketTest.java
new file mode 100644
index 0000000000..80a611abd8
--- /dev/null
+++ b/src/test/java/net/packet/ByteBufInPacketTest.java
@@ -0,0 +1,192 @@
+package net.packet;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.awt.*;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class ByteBufInPacketTest {
+ private ByteBuf byteBuf;
+ private InPacket inPacket;
+
+ @BeforeEach
+ void reset() {
+ this.byteBuf = Unpooled.buffer();
+ this.inPacket = new ByteBufInPacket(byteBuf);
+ }
+
+ private void givenWrittenBytes(int... bytes) {
+ for (int b : bytes) {
+ byteBuf.writeByte(b);
+ }
+ }
+
+ @Test
+ void readByte() {
+ final byte writtenByte = 123;
+ byteBuf.writeByte(writtenByte);
+
+ byte readByte = inPacket.readByte();
+
+ assertEquals(writtenByte, readByte);
+ }
+
+ @Test
+ void readShort() {
+ final short writtenShort = 12_345;
+ byteBuf.writeShortLE(writtenShort);
+
+ short readShort = inPacket.readShort();
+
+ assertEquals(writtenShort, readShort);
+ }
+
+ @Test
+ void readInt() {
+ final int writtenInt = 1_234_567_890;
+ byteBuf.writeIntLE(writtenInt);
+
+ int readInt = inPacket.readInt();
+
+ assertEquals(writtenInt, readInt);
+ }
+
+ @Test
+ void readLong() {
+ final long writtenLong = 9_223_372_036_854_775_807L;
+ byteBuf.writeLongLE(writtenLong);
+
+ long readLong = inPacket.readLong();
+
+ assertEquals(writtenLong, readLong);
+ }
+
+ @Test
+ void readPoint() {
+ final Point writtenPoint = new Point(111, 222);
+ byteBuf.writeShortLE((short) writtenPoint.getX());
+ byteBuf.writeShortLE((short) writtenPoint.getY());
+
+ Point readPoint = inPacket.readPoint();
+
+ assertEquals(writtenPoint, readPoint);
+ }
+
+ @Test
+ void readString() {
+ final String writtenString = "You have gained experience (+3200)";
+ byteBuf.writeShortLE(writtenString.length());
+ byte[] writtenStringBytes = writtenString.getBytes(Packet.STRING_CHARSET);
+ byteBuf.writeBytes(writtenStringBytes);
+
+ String readString = inPacket.readString();
+
+ assertEquals(writtenString, readString);
+ }
+
+ @Test
+ void readBytes() {
+ givenWrittenBytes(10, 11, 12, 13, 14, 15);
+
+ byte[] byteBatch1 = inPacket.readBytes(1);
+ assertEquals(1, byteBatch1.length);
+ assertEquals(10, byteBatch1[0]);
+
+ byte[] byteBatch2 = inPacket.readBytes(2);
+ assertEquals(2, byteBatch2.length);
+ assertEquals(11, byteBatch2[0]);
+ assertEquals(12, byteBatch2[1]);
+
+ byte[] byteBatch3 = inPacket.readBytes(3);
+ assertEquals(3, byteBatch3.length);
+ assertEquals(13, byteBatch3[0]);
+ assertEquals(14, byteBatch3[1]);
+ assertEquals(15, byteBatch3[2]);
+ }
+
+ @Test
+ void skip() {
+ givenWrittenBytes(20, 21, 22, 23, 24, 25);
+
+ byte firstByte = inPacket.readByte();
+ assertEquals(20, firstByte);
+
+ inPacket.skip(3);
+
+ byte fifthByte = inPacket.readByte();
+ assertEquals(24, fifthByte);
+ }
+
+ @Test
+ void available() {
+ givenWrittenBytes(30, 31, 32, 33, 34, 35);
+
+ assertEquals(6, inPacket.available());
+
+ inPacket.readByte();
+ assertEquals(5, inPacket.available());
+
+ inPacket.readInt();
+ assertEquals(1, inPacket.available());
+ }
+
+ @Test
+ void seek() {
+ givenWrittenBytes(40, 41, 42, 43, 44, 45);
+
+ inPacket.seek(2);
+ assertEquals(4, inPacket.available());
+ byte byteAtSeek = inPacket.readByte();
+ assertEquals(42, byteAtSeek);
+
+ inPacket.seek(0);
+ byte byteAtReset = inPacket.readByte();
+ assertEquals(40, byteAtReset);
+ }
+
+ @Test
+ void getPosition() {
+ givenWrittenBytes(50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60);
+
+ assertEquals(0, inPacket.getPosition());
+
+ inPacket.readByte();
+ assertEquals(1, inPacket.getPosition());
+
+ inPacket.readShort();
+ assertEquals(3, inPacket.getPosition());
+
+ inPacket.readInt();
+ assertEquals(7, inPacket.getPosition());
+
+ inPacket.seek(5);
+ assertEquals(5, inPacket.getPosition());
+ }
+
+ @Test
+ void getBytes() {
+ givenWrittenBytes(20, 19, 21, 18, 22);
+
+ byte[] bytes = inPacket.getBytes();
+
+ assertArrayEquals(new byte[]{20, 19, 21, 18, 22}, bytes);
+ }
+
+ @Test
+ void whenGetBytes_shouldBeRepeatable() {
+ givenWrittenBytes(1, 2, 3, 4, 5);
+
+ byte[] bytes = inPacket.getBytes();
+ assertEquals(5, bytes.length);
+
+ byte[] sameBytes = inPacket.getBytes();
+ assertEquals(5, sameBytes.length);
+
+ assertArrayEquals(bytes, sameBytes);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/net/packet/ByteBufOutPacketTest.java b/src/test/java/net/packet/ByteBufOutPacketTest.java
new file mode 100644
index 0000000000..3673254736
--- /dev/null
+++ b/src/test/java/net/packet/ByteBufOutPacketTest.java
@@ -0,0 +1,206 @@
+package net.packet;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import net.opcodes.SendOpcode;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.awt.*;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class ByteBufOutPacketTest {
+ private ByteBufOutPacket outPacket;
+
+ @BeforeEach
+ void reset() {
+ outPacket = new ByteBufOutPacket(SendOpcode.ADMIN_SHOP); // Any opcode will do
+ }
+
+ private static ByteBuf wrapExplicitlyWrittenBytes(OutPacket outPacket) {
+ byte[] packetBytes = outPacket.getBytes();
+ ByteBuf byteBuf = Unpooled.copiedBuffer(packetBytes);
+ byteBuf.readShortLE(); // Skip over opcode
+ return byteBuf;
+ }
+
+ @Test
+ void whenInstantiatingNew_shouldWriteOpcode() {
+ byte[] packetBytes = new ByteBufOutPacket(SendOpcode.NPC_TALK).getBytes();
+ assertEquals(2, packetBytes.length);
+ }
+
+ @Test
+ void getBytes() {
+ ByteBufOutPacket outPacket = new ByteBufOutPacket(SendOpcode.PING); // This opcode has value 0x11 = 17 in decimal
+ outPacket.writeByte(10);
+ outPacket.writeByte(20);
+ outPacket.writeByte(30);
+
+ byte[] bytes = outPacket.getBytes();
+
+ assertArrayEquals(new byte[]{(byte) 17, (byte) 0, (byte) 10, (byte) 20, (byte) 30}, bytes);
+ }
+
+ @Test
+ void writeByte() {
+ final byte writtenByte = 19;
+ outPacket.writeByte(writtenByte);
+
+ ByteBuf wrapped = wrapExplicitlyWrittenBytes(outPacket);
+ byte readByte = wrapped.readByte();
+
+ assertEquals(writtenByte, readByte);
+ }
+
+ @Test
+ void writeByteFromInt() {
+ final int writtenInt = 123;
+ outPacket.writeByte(writtenInt);
+
+ ByteBuf wrapped = wrapExplicitlyWrittenBytes(outPacket);
+ byte readByte = wrapped.readByte();
+
+ assertEquals(writtenInt, readByte);
+ }
+
+ @Test
+ void whenWritingByteFromInt_shouldOnlyWrite1Byte() {
+ final int writtenInt = Integer.MAX_VALUE;
+ outPacket.writeByte(writtenInt);
+
+ byte[] bytes = outPacket.getBytes();
+ assertEquals(2 + 1, bytes.length); // 2 for opcode
+ }
+
+ @Test
+ void writeBytes() {
+ byte[] writtenBytes = {101, 102, 103};
+ outPacket.writeBytes(writtenBytes);
+
+ ByteBuf wrapped = wrapExplicitlyWrittenBytes(outPacket);
+
+ assertEquals(101, wrapped.readByte());
+ assertEquals(102, wrapped.readByte());
+ assertEquals(103, wrapped.readByte());
+ }
+
+ @Test
+ void writeShort() {
+ final short writtenShort = 4312;
+ outPacket.writeShort(writtenShort);
+
+ ByteBuf wrapped = wrapExplicitlyWrittenBytes(outPacket);
+ short readShort = wrapped.readShortLE();
+
+ assertEquals(writtenShort, readShort);
+ }
+
+ @Test
+ void whenWritingShortFromInt_shouldOnlyWrite2Bytes() {
+ final int writtenInt = Integer.MAX_VALUE;
+ outPacket.writeShort(writtenInt);
+
+ byte[] bytes = outPacket.getBytes();
+ assertEquals(2 + 2, bytes.length); // 2 for opcode
+ }
+
+ @Test
+ void writeShortFromInt() {
+ final int writtenInt = 34_567;
+ outPacket.writeShort(writtenInt);
+
+ ByteBuf wrapped = wrapExplicitlyWrittenBytes(outPacket);
+ short readShort = wrapped.readShortLE();
+
+ assertEquals((short) writtenInt, readShort);
+ }
+
+ @Test
+ void writeInt() {
+ final int writtenInt = 1_010_101_010;
+ outPacket.writeInt(writtenInt);
+
+ ByteBuf wrapped = wrapExplicitlyWrittenBytes(outPacket);
+ int readInt = wrapped.readIntLE();
+
+ assertEquals(writtenInt, readInt);
+ }
+
+ @Test
+ void writeLong() {
+ final long writtenLong = 100_200_300_400_500_600L;
+ outPacket.writeLong(writtenLong);
+
+ ByteBuf wrapped = wrapExplicitlyWrittenBytes(outPacket);
+ long readLong = wrapped.readLongLE();
+
+ assertEquals(writtenLong, readLong);
+ }
+
+ @Test
+ void writeBoolean_true() {
+ outPacket.writeBoolean(true);
+
+ ByteBuf wrapped = wrapExplicitlyWrittenBytes(outPacket);
+ byte readByte = wrapped.readByte();
+
+ assertEquals(1, readByte);
+ }
+
+ @Test
+ void writeBoolean_false() {
+ outPacket.writeBoolean(false);
+
+ ByteBuf wrapped = wrapExplicitlyWrittenBytes(outPacket);
+ byte readByte = wrapped.readByte();
+
+ assertEquals(0, readByte);
+ }
+
+ @Test
+ void writeString() {
+ final String writtenString = "You've been weakened, making you unable to jump.";
+ outPacket.writeString(writtenString);
+
+ ByteBuf wrapped = wrapExplicitlyWrittenBytes(outPacket);
+ int length = wrapped.readShortLE();
+ byte[] stringBytes = new byte[length];
+ wrapped.readBytes(stringBytes);
+ String readString = new String(stringBytes, StandardCharsets.US_ASCII);
+
+ assertEquals(writtenString, readString);
+ }
+
+ @Test
+ void writePoint() {
+ final Point writtenPoint = new Point(23, 42);
+ outPacket.writePoint(writtenPoint);
+
+ ByteBuf wrapped = wrapExplicitlyWrittenBytes(outPacket);
+ short readX = wrapped.readShortLE();
+ short readY = wrapped.readShortLE();
+
+ assertEquals((short) writtenPoint.getX(), readX);
+ assertEquals((short) writtenPoint.getY(), readY);
+ }
+
+ @Test
+ void whenSkipping_shouldWriteZeroes() {
+ final byte firstWrittenByte = 9;
+ final byte secondWrittenByte = 11;
+ outPacket.writeByte(firstWrittenByte);
+ outPacket.skip(2);
+ outPacket.writeByte(secondWrittenByte);
+
+ ByteBuf wrapped = wrapExplicitlyWrittenBytes(outPacket);
+
+ assertEquals(firstWrittenByte, wrapped.readByte());
+ assertEquals(0, wrapped.readByte());
+ assertEquals(0, wrapped.readByte());
+ assertEquals(secondWrittenByte, wrapped.readByte());
+ }
+}
\ No newline at end of file