Skip to content

Commit

Permalink
Zones (#7)
Browse files Browse the repository at this point in the history
* feat: zone packets (excluding partial enclosed, as need to figure out the design of that first)

* chore: create an osrs-221-api module that will be the 'root' to bringing the entire application together from the server's perspective

* refactor: move ZoneProt and ZoneProtEncoder to revision-specific internal modules

* feat: UpdateZonePartialEnclosed initial implementation, subject to change if a better design gets figured out

* feat: update dictionary with new entries
  • Loading branch information
Z-Kris authored Apr 7, 2024
1 parent 2d7412e commit 1488dfa
Show file tree
Hide file tree
Showing 36 changed files with 2,115 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .idea/dictionaries/krist.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions protocol/osrs-221-api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
plugins {
alias(libs.plugins.kotlin.jvm)
}

dependencies {
api(libs.netty.buffer)
api(projects.protocol)
api(projects.compression)
api(projects.crypto)
api(projects.protocol.osrs221Shared)
api(projects.protocol.osrs221Model)
implementation(projects.protocol.osrs221Internal)
implementation(projects.protocol.osrs221Desktop)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package net.rsprot.protocol.game.outgoing.codec.zone.header

import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufAllocator
import io.netty.channel.ChannelHandlerContext
import net.rsprot.buffer.JagByteBuf
import net.rsprot.buffer.extensions.toJagByteBuf
import net.rsprot.protocol.ServerProt
import net.rsprot.protocol.game.outgoing.codec.zone.payload.LocAddChangeEncoder
import net.rsprot.protocol.game.outgoing.codec.zone.payload.LocAnimEncoder
import net.rsprot.protocol.game.outgoing.codec.zone.payload.LocDelEncoder
import net.rsprot.protocol.game.outgoing.codec.zone.payload.LocMergeEncoder
import net.rsprot.protocol.game.outgoing.codec.zone.payload.MapAnimEncoder
import net.rsprot.protocol.game.outgoing.codec.zone.payload.MapProjAnimEncoder
import net.rsprot.protocol.game.outgoing.codec.zone.payload.ObjAddEncoder
import net.rsprot.protocol.game.outgoing.codec.zone.payload.ObjCountEncoder
import net.rsprot.protocol.game.outgoing.codec.zone.payload.ObjDelEncoder
import net.rsprot.protocol.game.outgoing.codec.zone.payload.ObjOpFilterEncoder
import net.rsprot.protocol.game.outgoing.codec.zone.payload.SoundAreaEncoder
import net.rsprot.protocol.game.outgoing.prot.GameServerProt
import net.rsprot.protocol.game.outgoing.zone.header.UpdateZonePartialEnclosed
import net.rsprot.protocol.internal.game.outgoing.codec.zone.payload.ZoneProt
import net.rsprot.protocol.internal.game.outgoing.codec.zone.payload.ZoneProtEncoder
import net.rsprot.protocol.message.codec.MessageEncoder
import kotlin.math.min

public class DesktopUpdateZonePartialEnclosedEncoder : MessageEncoder<UpdateZonePartialEnclosed> {
override val prot: ServerProt = GameServerProt.UPDATE_ZONE_PARTIAL_ENCLOSED

override fun encode(
ctx: ChannelHandlerContext,
buffer: JagByteBuf,
message: UpdateZonePartialEnclosed,
) {
buffer.p1(message.zoneX)
buffer.p1Alt2(message.zoneZ)
buffer.p1Alt3(message.level)
buffer.buffer.writeBytes(
message.payload,
message.payload.readerIndex(),
message.payload.readableBytes(),
)
}

public companion object {
private const val MAX_PARTIAL_ENCLOSED_SIZE = 40_000 - 3

/**
* Builds a cache of a given zone's list of zone prots.
* This is intended so the server only requests one cache per zone per game cycle,
* rather than re-building the same buffer N times, where N is the number of players
* observing the zone. With this in mind however, zone prots which are player-specific,
* such as OBJ_ADD cannot be grouped together and must be sent separately, as they also
* are in OldSchool RuneScape.
* @param allocator the byte buffer allocator used for the cached buffer.
* Note that it is the server's responsibility to release the buffer once the cycle has ended.
* The individual writes of [UpdateZonePartialEnclosed] do not modify the reference count
* in any way.
* @param messages the list of zone prot messages to be encoded.
*/
public fun buildCache(
allocator: ByteBufAllocator,
messages: List<ZoneProt>,
): ByteBuf {
val buffer =
allocator.buffer(
min(IndexedZoneProtEncoder.maxZoneProtSize * messages.size, MAX_PARTIAL_ENCLOSED_SIZE),
MAX_PARTIAL_ENCLOSED_SIZE,
).toJagByteBuf()
for (message in messages) {
val indexedEncoder = IndexedZoneProtEncoder.indexedEncoders[message.protId]
buffer.p1(indexedEncoder.ordinal)
encodeMessage(
buffer,
message,
indexedEncoder.encoder,
)
}
return buffer.buffer
}

/**
* Encodes the [message] into the [buffer] using the [encoder] as the encoder for it.
* @param buffer the buffer to encode into
* @param message the message to be encoded
* @param encoder the encoder to use for encoding the message.
* Note that the type of the encoder is not compile-time known as we acquire it dynamically
* based on the message itself.
*/
private fun <T : ZoneProt> encodeMessage(
buffer: JagByteBuf,
message: T,
encoder: ZoneProtEncoder<*>,
) {
@Suppress("UNCHECKED_CAST")
encoder as ZoneProtEncoder<T>
encoder.encode(buffer, message)
}

/**
* Zone prot encoders here are used specifically by the [UpdateZonePartialEnclosed]
* packet, as this packet has its own sub-system of the zone prots, with the ability
* to send a batch of zone packets in one go with its own internal indexing.
*
* WARNING: This enum's order MUST match the order in the client, as the
* [IndexedZoneProtEncoder.ordinal] function is used for indexing!
*
* @property protId the respective [ZoneProt.protId] of each message, used for
* quick indexing of respective messages.
* @property encoder the zone prot encoder responsible for encoding the respective message
* into a byte buffer.
*/
private enum class IndexedZoneProtEncoder(
private val protId: Int,
val encoder: ZoneProtEncoder<*>,
) {
LOC_DEL(ZoneProt.LOC_DEL, LocDelEncoder()),
OBJ_DEL(ZoneProt.OBJ_DEL, ObjDelEncoder()),
LOC_ANIM(ZoneProt.LOC_ANIM, LocAnimEncoder()),
LOC_MERGE(ZoneProt.LOC_MERGE, LocMergeEncoder()),
OBJ_ADD(ZoneProt.OBJ_ADD, ObjAddEncoder()),
MAP_ANIM(ZoneProt.MAP_ANIM, MapAnimEncoder()),
OBJ_COUNT(ZoneProt.OBJ_COUNT, ObjCountEncoder()),
OBJ_OPFILTER(ZoneProt.OBJ_OPFILTER, ObjOpFilterEncoder()),
MAP_PROJANIM(ZoneProt.MAP_PROJANIM, MapProjAnimEncoder()),
SOUND_AREA(ZoneProt.SOUND_AREA, SoundAreaEncoder()),
LOC_ADD_CHANGE(ZoneProt.LOC_ADD_CHANGE, LocAddChangeEncoder()),
;

companion object {
/**
* The maximum possible size of a single zone prot.
* This constant is used to determine the maximum initial possible buffer capacity.
*/
val maxZoneProtSize =
entries.maxOf {
it.encoder.prot.size
}

/**
* The zone prot encoders indexed by their prot ids, allowing for fast access based
* on the respective [ZoneProt.protId] through the array.
*/
val indexedEncoders =
Array(entries.size) { index ->
entries.first { prot ->
index == prot.protId
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package net.rsprot.protocol.game.outgoing.codec.zone.header

import io.netty.channel.ChannelHandlerContext
import net.rsprot.buffer.JagByteBuf
import net.rsprot.protocol.ServerProt
import net.rsprot.protocol.game.outgoing.prot.GameServerProt
import net.rsprot.protocol.game.outgoing.zone.header.UpdateZoneFullFollows
import net.rsprot.protocol.message.codec.MessageEncoder

public class UpdateZoneFullFollowsEncoder : MessageEncoder<UpdateZoneFullFollows> {
override val prot: ServerProt = GameServerProt.UPDATE_ZONE_FULL_FOLLOWS

override fun encode(
ctx: ChannelHandlerContext,
buffer: JagByteBuf,
message: UpdateZoneFullFollows,
) {
buffer.p1(message.zoneZ)
buffer.p1(message.level)
buffer.p1Alt1(message.zoneX)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package net.rsprot.protocol.game.outgoing.codec.zone.header

import io.netty.channel.ChannelHandlerContext
import net.rsprot.buffer.JagByteBuf
import net.rsprot.protocol.ServerProt
import net.rsprot.protocol.game.outgoing.prot.GameServerProt
import net.rsprot.protocol.game.outgoing.zone.header.UpdateZonePartialFollows
import net.rsprot.protocol.message.codec.MessageEncoder

public class UpdateZonePartialFollowsEncoder : MessageEncoder<UpdateZonePartialFollows> {
override val prot: ServerProt = GameServerProt.UPDATE_ZONE_PARTIAL_FOLLOWS

override fun encode(
ctx: ChannelHandlerContext,
buffer: JagByteBuf,
message: UpdateZonePartialFollows,
) {
buffer.p1Alt3(message.zoneX)
buffer.p1(message.zoneZ)
buffer.p1Alt3(message.level)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package net.rsprot.protocol.game.outgoing.codec.zone.payload

import net.rsprot.buffer.JagByteBuf
import net.rsprot.protocol.ServerProt
import net.rsprot.protocol.game.outgoing.prot.GameServerProt
import net.rsprot.protocol.game.outgoing.zone.payload.LocAddChange
import net.rsprot.protocol.internal.game.outgoing.codec.zone.payload.ZoneProtEncoder

public class LocAddChangeEncoder : ZoneProtEncoder<LocAddChange> {
override val prot: ServerProt = GameServerProt.LOC_ADD_CHANGE

override fun encode(
buffer: JagByteBuf,
message: LocAddChange,
) {
// The function at the bottom of the LOC_ADD_CHANGE has a consistent order,
// making it easy to identify all the properties of this packet:
// loc_add_change_del(level, x, z, layer, id, shape, rotation, opFlags, 0, -1)
buffer.p2Alt2(message.id)
buffer.p1Alt1(message.coordInZonePacked)
buffer.p1Alt3(message.locPropertiesPacked)
buffer.p1Alt3(message.opFlags.value)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package net.rsprot.protocol.game.outgoing.codec.zone.payload

import net.rsprot.buffer.JagByteBuf
import net.rsprot.protocol.ServerProt
import net.rsprot.protocol.game.outgoing.prot.GameServerProt
import net.rsprot.protocol.game.outgoing.zone.payload.LocAnim
import net.rsprot.protocol.internal.game.outgoing.codec.zone.payload.ZoneProtEncoder

public class LocAnimEncoder : ZoneProtEncoder<LocAnim> {
override val prot: ServerProt = GameServerProt.LOC_ANIM

override fun encode(
buffer: JagByteBuf,
message: LocAnim,
) {
// The function at the bottom of the LOC_ANIM has a consistent order,
// making it easy to identify all the properties of this packet:
// loc_anim(level, x, z, shape, rotation, layer, id)
buffer.p2Alt2(message.id)
buffer.p1(message.locPropertiesPacked)
buffer.p1Alt3(message.coordInZonePacked)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package net.rsprot.protocol.game.outgoing.codec.zone.payload

import net.rsprot.buffer.JagByteBuf
import net.rsprot.protocol.ServerProt
import net.rsprot.protocol.game.outgoing.prot.GameServerProt
import net.rsprot.protocol.game.outgoing.zone.payload.LocDel
import net.rsprot.protocol.internal.game.outgoing.codec.zone.payload.ZoneProtEncoder

public class LocDelEncoder : ZoneProtEncoder<LocDel> {
override val prot: ServerProt = GameServerProt.LOC_DEL

override fun encode(
buffer: JagByteBuf,
message: LocDel,
) {
// The function at the bottom of the LOC_DEL has a consistent order,
// making it easy to identify all the properties of this packet:
// loc_add_change_del(level, x, z, layer, -1, shape, rotation, 31, 0, -1)
buffer.p1Alt3(message.coordInZonePacked)
buffer.p1Alt3(message.locPropertiesPacked)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package net.rsprot.protocol.game.outgoing.codec.zone.payload

import net.rsprot.buffer.JagByteBuf
import net.rsprot.protocol.ServerProt
import net.rsprot.protocol.game.outgoing.prot.GameServerProt
import net.rsprot.protocol.game.outgoing.zone.payload.LocMerge
import net.rsprot.protocol.internal.game.outgoing.codec.zone.payload.ZoneProtEncoder

public class LocMergeEncoder : ZoneProtEncoder<LocMerge> {
override val prot: ServerProt = GameServerProt.LOC_MERGE

override fun encode(
buffer: JagByteBuf,
message: LocMerge,
) {
// The function at the bottom of the LOC_MERGE has a consistent order,
// making it easy to identify all the properties of this packet:
// loc_merge(level, x, z, shape, rotation, layer, id, start, end, minX, minZ, maxX, maxZ, player)
buffer.p1Alt3(message.minX)
buffer.p1Alt3(message.minZ)
buffer.p2Alt2(message.index)
buffer.p2(message.id)
buffer.p1Alt2(message.locPropertiesPacked)
buffer.p1Alt3(message.maxX)
buffer.p2Alt1(message.start)
buffer.p2Alt1(message.end)
buffer.p1(message.coordInZonePacked)
buffer.p1Alt2(message.maxZ)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package net.rsprot.protocol.game.outgoing.codec.zone.payload

import net.rsprot.buffer.JagByteBuf
import net.rsprot.protocol.ServerProt
import net.rsprot.protocol.game.outgoing.prot.GameServerProt
import net.rsprot.protocol.game.outgoing.zone.payload.MapAnim
import net.rsprot.protocol.internal.game.outgoing.codec.zone.payload.ZoneProtEncoder

public class MapAnimEncoder : ZoneProtEncoder<MapAnim> {
override val prot: ServerProt = GameServerProt.MAP_ANIM

override fun encode(
buffer: JagByteBuf,
message: MapAnim,
) {
// While MAP_ANIM does not have a common function like the rest,
// the constructor for the SpotAnimation object itself has the following order:
// SpotAnimation(id, level, fineX, fineZ, getGroundHeight(fineX, fineZ, level) - height, delay, cycle)
buffer.p1Alt1(message.coordInZonePacked)
buffer.p2Alt1(message.delay)
buffer.p2(message.id)
buffer.p1Alt1(message.height)
}
}
Loading

0 comments on commit 1488dfa

Please sign in to comment.