diff --git a/pom.xml b/pom.xml
index 5c38554..a41903f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -9,13 +9,23 @@
Progressive Java Client
+
17
17
UTF-8
+ 2.0.21
+ 1.8
3.3.5
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib-jdk8
+ ${kotlin.version}
+
+
org.lwjgl
@@ -161,6 +171,27 @@
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+ ${kotlin.version}
+
+
+ kotlin-compile
+ process-sources
+ compile
+
+ ${kotlin.compiler.jvmTarget}
+
+ ${project.basedir}/src/main/kotlin
+ ${project.basedir}/src/main/java
+
+
+
+
+
+
org.apache.maven.plugins
maven-compiler-plugin
diff --git a/src/main/java/com/gradwahl/rs254/io/IsaacCipher.java b/src/main/java/com/gradwahl/rs254/io/IsaacCipher.java
deleted file mode 100644
index b10cd34..0000000
--- a/src/main/java/com/gradwahl/rs254/io/IsaacCipher.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package com.gradwahl.rs254.io;
-
-public final class IsaacCipher {
- private int count;
- private final int[] rsl = new int[256];
- private final int[] mem = new int[256];
- private int a;
- private int b;
- private int c;
-
- public IsaacCipher(int[] seed) {
- System.arraycopy(seed, 0, rsl, 0, Math.min(seed.length, rsl.length));
- init();
- }
-
- public int nextInt() {
- if (count-- == 0) {
- isaac();
- count = 255;
- }
- return rsl[count];
- }
-
- private void mix(int[] x) {
- x[0] ^= x[1] << 11; x[3] += x[0]; x[1] += x[2];
- x[1] ^= x[2] >>> 2; x[4] += x[1]; x[2] += x[3];
- x[2] ^= x[3] << 8; x[5] += x[2]; x[3] += x[4];
- x[3] ^= x[4] >>> 16; x[6] += x[3]; x[4] += x[5];
- x[4] ^= x[5] << 10; x[7] += x[4]; x[5] += x[6];
- x[5] ^= x[6] >>> 4; x[0] += x[5]; x[6] += x[7];
- x[6] ^= x[7] << 8; x[1] += x[6]; x[7] += x[0];
- x[7] ^= x[0] >>> 9; x[2] += x[7]; x[0] += x[1];
- }
-
- private void init() {
- int[] x = new int[8];
- for (int i = 0; i < x.length; i++) x[i] = 0x9e3779b9;
- for (int i = 0; i < 4; i++) mix(x);
-
- for (int i = 0; i < 256; i += 8) {
- for (int j = 0; j < 8; j++) x[j] += rsl[i + j];
- mix(x);
- System.arraycopy(x, 0, mem, i, 8);
- }
-
- for (int i = 0; i < 256; i += 8) {
- for (int j = 0; j < 8; j++) x[j] += mem[i + j];
- mix(x);
- System.arraycopy(x, 0, mem, i, 8);
- }
-
- isaac();
- count = 256;
- }
-
- private void isaac() {
- b += ++c;
- for (int i = 0; i < 256; i++) {
- int x = mem[i];
- switch (i & 3) {
- case 0 -> a ^= a << 13;
- case 1 -> a ^= a >>> 6;
- case 2 -> a ^= a << 2;
- case 3 -> a ^= a >>> 16;
- default -> throw new IllegalStateException();
- }
- a += mem[(i + 128) & 0xff];
- int y = mem[i] = mem[(x >>> 2) & 0xff] + a + b;
- rsl[i] = b = mem[(y >>> 10) & 0xff] + x;
- }
- }
-}
diff --git a/src/main/java/com/gradwahl/rs254/io/PacketBuffer.java b/src/main/java/com/gradwahl/rs254/io/PacketBuffer.java
deleted file mode 100644
index 42f0a7f..0000000
--- a/src/main/java/com/gradwahl/rs254/io/PacketBuffer.java
+++ /dev/null
@@ -1,85 +0,0 @@
-package com.gradwahl.rs254.io;
-
-import java.math.BigInteger;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.zip.CRC32;
-
-public final class PacketBuffer {
- public final byte[] data;
- public int pos;
- public IsaacCipher random;
-
- public PacketBuffer(int size) {
- this.data = new byte[size];
- }
-
- public PacketBuffer(byte[] data) {
- this.data = data;
- }
-
- public int g1() { return data[pos++] & 0xff; }
-
- public int g4() {
- return ((data[pos++] & 0xff) << 24)
- | ((data[pos++] & 0xff) << 16)
- | ((data[pos++] & 0xff) << 8)
- | (data[pos++] & 0xff);
- }
-
- public long g8() {
- return ((long) g4() << 32) | (g4() & 0xffffffffL);
- }
-
- public void p1(int value) { data[pos++] = (byte) value; }
-
- public void p2(int value) {
- data[pos++] = (byte) (value >>> 8);
- data[pos++] = (byte) value;
- }
-
- public void p4(int value) {
- data[pos++] = (byte) (value >>> 24);
- data[pos++] = (byte) (value >>> 16);
- data[pos++] = (byte) (value >>> 8);
- data[pos++] = (byte) value;
- }
-
- public void pjstr(String value) {
- byte[] bytes = value.getBytes(StandardCharsets.ISO_8859_1);
- pdata(bytes, 0, bytes.length);
- p1(10);
- }
-
- public void pdata(byte[] src, int off, int len) {
- System.arraycopy(src, off, data, pos, len);
- pos += len;
- }
-
- public void pIsaac(int opcode) {
- if (random == null) throw new IllegalStateException("ISAAC is not initialised");
- p1((opcode + random.nextInt()) & 0xff);
- }
-
- public byte[] bytes() {
- return Arrays.copyOf(data, pos);
- }
-
- public void rsaenc(BigInteger modulus, BigInteger exponent) {
- byte[] raw = Arrays.copyOf(data, pos);
- BigInteger rawInt = new BigInteger(1, raw);
- byte[] encrypted = rawInt.modPow(exponent, modulus).toByteArray();
- if (encrypted.length > 0 && encrypted[0] == 0) {
- encrypted = Arrays.copyOfRange(encrypted, 1, encrypted.length);
- }
- pos = 0;
- p1(encrypted.length);
- pdata(encrypted, 0, encrypted.length);
- }
-
- public static int crc32(byte[] src, int off, int len) {
- CRC32 crc = new CRC32();
- crc.update(src, off, len);
- return (int) crc.getValue();
- }
-}
diff --git a/src/main/java/com/gradwahl/rs254/io/Protocol.java b/src/main/java/com/gradwahl/rs254/io/Protocol.java
deleted file mode 100644
index ca9e18c..0000000
--- a/src/main/java/com/gradwahl/rs254/io/Protocol.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package com.gradwahl.rs254.io;
-
-public final class Protocol {
- private Protocol() {}
-
- public static final class Client {
- public static final int NO_TIMEOUT = 239;
- public static final int MOVE_GAMECLICK = 6;
- public static final int MESSAGE_PUBLIC = 83;
- public static final int CLIENT_CHEAT = 86;
- public static final int FRIENDLIST_ADD = 9;
- public static final int FRIENDLIST_DEL = 84;
- public static final int CHAT_SETMODE = 129;
- }
-
- public static final class Server {
- public static final int LOGOUT = 21;
- public static final int MESSAGE_GAME = 73;
- public static final int PLAYER_INFO = 87;
- public static final int NPC_INFO = 123;
- public static final int REBUILD_NORMAL = 209;
-
- public static final int[] SIZES = {
- 6, 0, 0, 4, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 4, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 3, 5, 0, 6, -2, 0, 4, 0,
- 0, 0, 0, 0, 0, 15, 4, 0, 0, -2, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 1, 0, -1, -2, 0, -2,
- 6, 0, 0, 0, 0, 0, 4, 0, 0, -1, 0, 1, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 2, 0, -2, 2, 0, 0, 3, 0, 0, 1, 4, 0, 0,
- 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 6, 3,
- 0, 0, 0, 0, 5, 0, 0, -2, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0,
- 0, 0, 6, 0, 1, 0, 0, 2, 0, 2, 0, 0, 10, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 2, 2, 0, 0, 0, 2, 0,
- -2, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 2,
- 0, 0, 0, 0, 0, 0, 0, 0, 6, 2, 0, 0, 0, 0, 0, 0, -1, 0,
- 0, 0, 0, 4, 0, 4, 0, 3, 0, 0, 0, 0, 14, 0, 0, 0, 6, 0,
- 0, 4, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0,
- 4, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 1, 0
- };
- }
-}
diff --git a/src/main/java/com/gradwahl/rs254/net/LoginResult.java b/src/main/java/com/gradwahl/rs254/net/LoginResult.java
deleted file mode 100644
index 5c2aa8d..0000000
--- a/src/main/java/com/gradwahl/rs254/net/LoginResult.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.gradwahl.rs254.net;
-
-public record LoginResult(int response, int staffModLevel, boolean mouseTracking) {
- public boolean success() { return response == 2; }
-
- @Override
- public String toString() {
- return switch (response) {
- case 2 -> "Login OK. staff=" + staffModLevel + ", mouseTracking=" + mouseTracking;
- case 3 -> "Invalid username or password.";
- case 4 -> "Account disabled.";
- case 5 -> "Already logged in.";
- case 6 -> "Client out of date / revision or CRC mismatch.";
- case 7 -> "World full.";
- case 16 -> "Too many login attempts.";
- default -> "Login response " + response;
- };
- }
-}
diff --git a/src/main/kotlin/com/gradwahl/rs254/io/IsaacCipher.kt b/src/main/kotlin/com/gradwahl/rs254/io/IsaacCipher.kt
new file mode 100644
index 0000000..4f9e5a2
--- /dev/null
+++ b/src/main/kotlin/com/gradwahl/rs254/io/IsaacCipher.kt
@@ -0,0 +1,79 @@
+package com.gradwahl.rs254.io
+
+class IsaacCipher(seed: IntArray) {
+ private var count = 0
+ private val rsl = IntArray(256)
+ private val mem = IntArray(256)
+ private var a = 0
+ private var b = 0
+ private var c = 0
+
+ init {
+ System.arraycopy(seed, 0, rsl, 0, minOf(seed.size, rsl.size))
+ initState()
+ }
+
+ fun nextInt(): Int {
+ val previous = count
+ count--
+ if (previous == 0) {
+ isaac()
+ count = 255
+ }
+ return rsl[count]
+ }
+
+ private fun mix(x: IntArray) {
+ x[0] = x[0] xor (x[1] shl 11); x[3] += x[0]; x[1] += x[2]
+ x[1] = x[1] xor (x[2] ushr 2); x[4] += x[1]; x[2] += x[3]
+ x[2] = x[2] xor (x[3] shl 8); x[5] += x[2]; x[3] += x[4]
+ x[3] = x[3] xor (x[4] ushr 16); x[6] += x[3]; x[4] += x[5]
+ x[4] = x[4] xor (x[5] shl 10); x[7] += x[4]; x[5] += x[6]
+ x[5] = x[5] xor (x[6] ushr 4); x[0] += x[5]; x[6] += x[7]
+ x[6] = x[6] xor (x[7] shl 8); x[1] += x[6]; x[7] += x[0]
+ x[7] = x[7] xor (x[0] ushr 9); x[2] += x[7]; x[0] += x[1]
+ }
+
+ private fun initState() {
+ val x = IntArray(8) { 0x9e3779b9L.toInt() }
+ repeat(4) { mix(x) }
+
+ var i = 0
+ while (i < 256) {
+ for (j in 0 until 8) x[j] += rsl[i + j]
+ mix(x)
+ System.arraycopy(x, 0, mem, i, 8)
+ i += 8
+ }
+
+ i = 0
+ while (i < 256) {
+ for (j in 0 until 8) x[j] += mem[i + j]
+ mix(x)
+ System.arraycopy(x, 0, mem, i, 8)
+ i += 8
+ }
+
+ isaac()
+ count = 256
+ }
+
+ private fun isaac() {
+ c++
+ b += c
+ for (i in 0 until 256) {
+ val x = mem[i]
+ when (i and 3) {
+ 0 -> a = a xor (a shl 13)
+ 1 -> a = a xor (a ushr 6)
+ 2 -> a = a xor (a shl 2)
+ 3 -> a = a xor (a ushr 16)
+ }
+ a += mem[(i + 128) and 0xff]
+ val y = mem[(x ushr 2) and 0xff] + a + b
+ mem[i] = y
+ b = mem[(y ushr 10) and 0xff] + x
+ rsl[i] = b
+ }
+ }
+}
diff --git a/src/main/kotlin/com/gradwahl/rs254/io/PacketBuffer.kt b/src/main/kotlin/com/gradwahl/rs254/io/PacketBuffer.kt
new file mode 100644
index 0000000..5c34468
--- /dev/null
+++ b/src/main/kotlin/com/gradwahl/rs254/io/PacketBuffer.kt
@@ -0,0 +1,92 @@
+package com.gradwahl.rs254.io
+
+import java.math.BigInteger
+import java.util.zip.CRC32
+
+class PacketBuffer {
+ @JvmField
+ val data: ByteArray
+
+ @JvmField
+ var pos: Int = 0
+
+ @JvmField
+ var random: IsaacCipher? = null
+
+ constructor(size: Int) {
+ data = ByteArray(size)
+ }
+
+ constructor(data: ByteArray) {
+ this.data = data
+ }
+
+ fun g1(): Int {
+ val value = data[pos].toInt() and 0xff
+ pos++
+ return value
+ }
+
+ fun g4(): Int =
+ (g1() shl 24) or
+ (g1() shl 16) or
+ (g1() shl 8) or
+ g1()
+
+ fun g8(): Long = (g4().toLong() shl 32) or (g4().toLong() and 0xffffffffL)
+
+ fun p1(value: Int) {
+ data[pos++] = value.toByte()
+ }
+
+ fun p2(value: Int) {
+ data[pos++] = (value ushr 8).toByte()
+ data[pos++] = value.toByte()
+ }
+
+ fun p4(value: Int) {
+ data[pos++] = (value ushr 24).toByte()
+ data[pos++] = (value ushr 16).toByte()
+ data[pos++] = (value ushr 8).toByte()
+ data[pos++] = value.toByte()
+ }
+
+ fun pjstr(value: String) {
+ val bytes = value.toByteArray(Charsets.ISO_8859_1)
+ pdata(bytes, 0, bytes.size)
+ p1(10)
+ }
+
+ fun pdata(src: ByteArray, off: Int, len: Int) {
+ System.arraycopy(src, off, data, pos, len)
+ pos += len
+ }
+
+ fun pIsaac(opcode: Int) {
+ val cipher = random ?: throw IllegalStateException("ISAAC is not initialised")
+ p1((opcode + cipher.nextInt()) and 0xff)
+ }
+
+ fun bytes(): ByteArray = data.copyOf(pos)
+
+ fun rsaenc(modulus: BigInteger, exponent: BigInteger) {
+ val raw = data.copyOf(pos)
+ val rawInt = BigInteger(1, raw)
+ var encrypted = rawInt.modPow(exponent, modulus).toByteArray()
+ if (encrypted.isNotEmpty() && encrypted[0].toInt() == 0) {
+ encrypted = encrypted.copyOfRange(1, encrypted.size)
+ }
+ pos = 0
+ p1(encrypted.size)
+ pdata(encrypted, 0, encrypted.size)
+ }
+
+ companion object {
+ @JvmStatic
+ fun crc32(src: ByteArray, off: Int, len: Int): Int {
+ val crc = CRC32()
+ crc.update(src, off, len)
+ return crc.value.toInt()
+ }
+ }
+}
diff --git a/src/main/kotlin/com/gradwahl/rs254/io/Protocol.kt b/src/main/kotlin/com/gradwahl/rs254/io/Protocol.kt
new file mode 100644
index 0000000..660d29f
--- /dev/null
+++ b/src/main/kotlin/com/gradwahl/rs254/io/Protocol.kt
@@ -0,0 +1,44 @@
+package com.gradwahl.rs254.io
+
+class Protocol private constructor() {
+ class Client private constructor() {
+ companion object {
+ const val NO_TIMEOUT = 239
+ const val MOVE_GAMECLICK = 6
+ const val MESSAGE_PUBLIC = 83
+ const val CLIENT_CHEAT = 86
+ const val FRIENDLIST_ADD = 9
+ const val FRIENDLIST_DEL = 84
+ const val CHAT_SETMODE = 129
+ }
+ }
+
+ class Server private constructor() {
+ companion object {
+ const val LOGOUT = 21
+ const val MESSAGE_GAME = 73
+ const val PLAYER_INFO = 87
+ const val NPC_INFO = 123
+ const val REBUILD_NORMAL = 209
+
+ @JvmField
+ val SIZES = intArrayOf(
+ 6, 0, 0, 4, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 4, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 3, 5, 0, 6, -2, 0, 4, 0,
+ 0, 0, 0, 0, 0, 15, 4, 0, 0, -2, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 1, 0, -1, -2, 0, -2,
+ 6, 0, 0, 0, 0, 0, 4, 0, 0, -1, 0, 1, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 2, 0, -2, 2, 0, 0, 3, 0, 0, 1, 4, 0, 0,
+ 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 6, 3,
+ 0, 0, 0, 0, 5, 0, 0, -2, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0,
+ 0, 0, 6, 0, 1, 0, 0, 2, 0, 2, 0, 0, 10, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 2, 2, 0, 0, 0, 2, 0,
+ -2, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 2,
+ 0, 0, 0, 0, 0, 0, 0, 0, 6, 2, 0, 0, 0, 0, 0, 0, -1, 0,
+ 0, 0, 0, 4, 0, 4, 0, 3, 0, 0, 0, 0, 14, 0, 0, 0, 6, 0,
+ 0, 4, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0,
+ 4, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 1, 0
+ )
+ }
+ }
+}
diff --git a/src/main/kotlin/com/gradwahl/rs254/net/LoginResult.kt b/src/main/kotlin/com/gradwahl/rs254/net/LoginResult.kt
new file mode 100644
index 0000000..d7ab2c6
--- /dev/null
+++ b/src/main/kotlin/com/gradwahl/rs254/net/LoginResult.kt
@@ -0,0 +1,26 @@
+package com.gradwahl.rs254.net
+
+data class LoginResult(
+ private val responseValue: Int,
+ private val staffModLevelValue: Int,
+ private val mouseTrackingValue: Boolean
+) {
+ fun response(): Int = responseValue
+
+ fun staffModLevel(): Int = staffModLevelValue
+
+ fun mouseTracking(): Boolean = mouseTrackingValue
+
+ fun success(): Boolean = responseValue == 2
+
+ override fun toString(): String = when (responseValue) {
+ 2 -> "Login OK. staff=$staffModLevelValue, mouseTracking=$mouseTrackingValue"
+ 3 -> "Invalid username or password."
+ 4 -> "Account disabled."
+ 5 -> "Already logged in."
+ 6 -> "Client out of date / revision or CRC mismatch."
+ 7 -> "World full."
+ 16 -> "Too many login attempts."
+ else -> "Login response $responseValue"
+ }
+}