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" + } +}