Skip to content

Commit

Permalink
feat: map packets
Browse files Browse the repository at this point in the history
  • Loading branch information
Z-Kris committed Apr 8, 2024
1 parent ecedc1e commit 406f049
Show file tree
Hide file tree
Showing 10 changed files with 497 additions and 18 deletions.
46 changes: 46 additions & 0 deletions crypto/src/main/kotlin/net/rsprot/crypto/util/XteaKey.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package net.rsprot.crypto.util

public class XteaKey(
public val key: IntArray,
) {
public constructor(
key1: Int,
key2: Int,
key3: Int,
key4: Int,
) : this(
intArrayOf(
key1,
key2,
key3,
key4,
),
)

init {
require(key.size == 4) {
"Xtea keys must be 128 bits in length (4 integers)"
}
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as XteaKey

return key.contentEquals(other.key)
}

override fun hashCode(): Int {
return key.contentHashCode()
}

override fun toString(): String {
return "XteaKey(key=${key.contentToString()})"
}

public companion object {
public val ZERO: XteaKey = XteaKey(0, 0, 0, 0)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package net.rsprot.protocol.game.outgoing.codec.map

import io.netty.channel.ChannelHandlerContext
import net.rsprot.buffer.JagByteBuf
import net.rsprot.protocol.ServerProt
import net.rsprot.protocol.game.outgoing.map.RebuildNormal
import net.rsprot.protocol.game.outgoing.prot.GameServerProt
import net.rsprot.protocol.message.codec.MessageEncoder

public class RebuildNormalEncoder : MessageEncoder<RebuildNormal> {
override val prot: ServerProt = GameServerProt.REBUILD_NORMAL

override fun encode(
ctx: ChannelHandlerContext,
buffer: JagByteBuf,
message: RebuildNormal,
) {
buffer.p2Alt3(message.zoneX)
buffer.p2Alt3(message.zoneZ)
// Currently unused property, unknown what it is for, presumably sailing-related
buffer.p2Alt2(0)
buffer.p2(message.keys.size)
for (xteaKey in message.keys) {
for (intKey in xteaKey.key) {
buffer.p4(intKey)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package net.rsprot.protocol.game.outgoing.codec.map

import io.netty.channel.ChannelHandlerContext
import net.rsprot.buffer.JagByteBuf
import net.rsprot.buffer.bitbuffer.toBitBuf
import net.rsprot.crypto.util.XteaKey
import net.rsprot.protocol.ServerProt
import net.rsprot.protocol.game.outgoing.map.RebuildRegion
import net.rsprot.protocol.game.outgoing.prot.GameServerProt
import net.rsprot.protocol.message.codec.MessageEncoder

public class RebuildRegionEncoder : MessageEncoder<RebuildRegion> {
override val prot: ServerProt = GameServerProt.REBUILD_REGION

override fun encode(
ctx: ChannelHandlerContext,
buffer: JagByteBuf,
message: RebuildRegion,
) {
buffer.p2(message.zoneX)
buffer.p2Alt2(message.zoneZ)
buffer.p1Alt2(if (message.reload) 1 else 0)

// Xtea count, temporary value
val marker = buffer.writerIndex()
buffer.p2(0)

var xteaCount = 0
val (mapsquares, xteas) = distinctMapsquares.get()
val bitbuf = buffer.buffer.toBitBuf()
bitbuf.use {
for (zone in message.zones) {
if (zone == null) {
bitbuf.pBits(1, 0)
continue
}
bitbuf.pBits(1, 1)
bitbuf.pBits(26, zone.referenceZone.packed)
val mapsquareId = zone.referenceZone.mapsquareId
if (contains(mapsquares, xteaCount, mapsquareId)) {
continue
}
mapsquares[xteaCount] = mapsquareId
xteas[xteaCount] = zone.key
xteaCount++
}
}
// Write the real xtea count
val writerIndex = buffer.writerIndex()
buffer.writerIndex(marker)
buffer.p2(xteaCount)
buffer.writerIndex(writerIndex)

for (i in 0..<xteaCount) {
val xteaKey = xteas[i]
for (intKey in xteaKey.key) {
buffer.p4(intKey)
}
}
}

/**
* Check if the [array] contains the [value] in it, up until [length] (exclusive).
* As our arrays are pre-initialized to a capacity of 676, we do not want to search
* the entire thing when we have only added a few elements to it.
* Additionally, since we do not zero out the arrays, anything beyond the [length]
* would be phantom data from previous packets.
* @param array the int array to search
* @param length the length of the array that has been filled up
* @param value the value to seek for
* @return whether the int array contains the [value] in the first [length] indices
*/
private fun contains(
array: IntArray,
length: Int,
value: Int,
): Boolean {
for (i in 0..<length) {
val element = array[i]
if (element == value) {
return true
}
}
return false
}

private companion object {
/**
* The maximum theoretical number of mapsquares that can be sent in a single
* rebuild region packet.
*/
private const val MAX_POTENTIAL_MAPSQUARES = 4 * 13 * 13

/**
* A thread-local implementation of mapsquares and their keys.
* As we need to trim our data set down to distinct mapsquares,
* doing so with new lists all the time can be quite wasteful, especially
* knowing how volatile the actual counts can be.
* To minimize the garbage created (in this case, to none),
* we use thread-local implementations for distinct mapsquares.
*/
private val distinctMapsquares =
ThreadLocal.withInitial {
IntArray(MAX_POTENTIAL_MAPSQUARES) to
Array(4 * 13 * 13) {
XteaKey.ZERO
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package net.rsprot.protocol.loginprot.incoming.codec

import net.rsprot.buffer.JagByteBuf
import net.rsprot.crypto.util.XteaKey
import net.rsprot.protocol.ClientProt
import net.rsprot.protocol.loginprot.incoming.GameReconnect
import net.rsprot.protocol.loginprot.incoming.codec.shared.LoginBlockDecoder
import net.rsprot.protocol.loginprot.incoming.prot.LoginClientProt
import net.rsprot.protocol.loginprot.incoming.util.XteaKey
import net.rsprot.protocol.message.codec.MessageDecoder
import net.rsprot.protocol.tools.MessageDecodingTools
import java.math.BigInteger
Expand Down
1 change: 1 addition & 0 deletions protocol/osrs-221-model/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ dependencies {
implementation(libs.inline.logger)
implementation(projects.buffer)
implementation(projects.compression)
implementation(projects.crypto)
implementation(projects.protocol)
implementation(projects.protocol.osrs221Internal)
implementation(projects.protocol.osrs221Shared)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package net.rsprot.protocol.game.outgoing.map

import net.rsprot.crypto.util.XteaKey
import net.rsprot.protocol.game.outgoing.map.util.XteaProvider
import net.rsprot.protocol.message.OutgoingMessage

/**
* Rebuild normal is sent when the game requires a map reload without being in instances.
* @property zoneX the x coordinate of the local player's current zone.
* @property zoneZ the z coordinate of the local player's current zone.
* @property keys the list of xtea keys needed to decrypt the map.
*/
public class RebuildNormal private constructor(
private val _zoneX: UShort,
private val _zoneZ: UShort,
public val keys: List<XteaKey>,
) : OutgoingMessage {
public constructor(
zoneX: Int,
zoneZ: Int,
keyProvider: XteaProvider,
) : this(
zoneX.toUShort(),
zoneZ.toUShort(),
buildXteaKeyList(zoneX, zoneZ, keyProvider),
)

public val zoneX: Int
get() = _zoneX.toInt()
public val zoneZ: Int
get() = _zoneZ.toInt()

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as RebuildNormal

if (_zoneX != other._zoneX) return false
if (_zoneZ != other._zoneZ) return false
if (keys != other.keys) return false

return true
}

override fun hashCode(): Int {
var result = _zoneX.hashCode()
result = 31 * result + _zoneZ.hashCode()
result = 31 * result + keys.hashCode()
return result
}

override fun toString(): String {
return "RebuildNormal(" +
"zoneX=$zoneX, " +
"zoneZ=$zoneZ, " +
"keys=$keys" +
")"
}

private companion object {
/**
* A helper function to build the mapsquare key list the same way the client does,
* as the keys must be in the same specific order as the client reads it.
*/
private fun buildXteaKeyList(
zoneX: Int,
zoneZ: Int,
keyProvider: XteaProvider,
): List<XteaKey> {
val minMapsquareX = (zoneX - 6) ushr 3
val maxMapsquareX = (zoneX + 6) ushr 3
val minMapsquareZ = (zoneZ - 6) ushr 3
val maxMapsquareZ = (zoneZ + 6) ushr 3
val count = (maxMapsquareX - minMapsquareZ + 1) * (maxMapsquareZ - minMapsquareZ + 1)
val keys = ArrayList<XteaKey>(count)
for (mapsquareX in minMapsquareX..maxMapsquareX) {
for (mapsquareZ in minMapsquareZ..maxMapsquareZ) {
keys += keyProvider.provide((mapsquareX shl 8) or mapsquareZ)
}
}
return keys
}
}
}
Loading

0 comments on commit 406f049

Please sign in to comment.