Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# RSProt

[![GitHub Actions][actions-badge]][actions] [![MIT license][mit-badge]][mit]
[![OldSchool - 221 - 239 (Alpha)](https://img.shields.io/badge/OldSchool-221--239_(Alpha)-9a1abd)](https://github.com/blurite/rsprot/tree/master/protocol/osrs-239/osrs-239-api/src/main/kotlin/net/rsprot/protocol/api)
[![OldSchool - 221 - 240 (Alpha)](https://img.shields.io/badge/OldSchool-221--240_(Alpha)-9a1abd)](https://github.com/blurite/rsprot/tree/master/protocol/osrs-240/osrs-240-api/src/main/kotlin/net/rsprot/protocol/api)

## Status
> [!NOTE]
Expand All @@ -16,7 +16,7 @@ In order to add it to your server, add the below line under dependencies
in your build.gradle.kts.

```kts
implementation("net.rsprot:osrs-239-api:1.0.0-ALPHA-20260718")
implementation("net.rsprot:osrs-240-api:1.0.0-ALPHA-20260804")
```

An in-depth tutorial on how to implement it will be added into this read-me
Expand All @@ -32,12 +32,12 @@ other revisions are welcome, but will not be provided by default.
- Java 11

## Supported Versions
This library currently supports revision 221-239 OldSchool desktop clients.
This library currently supports revision 221-240 OldSchool desktop clients.

## Quick Guide
This section covers a quick guide for how to use the protocol after implementing
the base API. It is not a guide for the base API itself, that will come in the
future. This specific quick guide refers to revision 239. Revisions older
future. This specific quick guide refers to revision 240. Revisions older
than 235 have a significantly different API and will not be explored here.
It is recommented you upgrade to latest, or view an older readme in history.

Expand Down
12 changes: 12 additions & 0 deletions WHATSNEW.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
## What's New?

### Revision 240

#### Additions
- A new serverprot with a size of -2 that has no handler in neither Java nor native.
- A new version of the spotanim extended info on players and NPCs. Each spotanim
now comes with a boolean for "loops", which tells the client to keep looping that
spotanim if the seq type of that spotanim allows looping.

#### Changes
- CLIENT_CHEAT packet now sends a byte after the cheat. The value is a constant
zero on Java.

### Revision 239

> [!NOTE]
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ plugins {

allprojects {
group = "net.rsprot"
version = "1.0.0-ALPHA-20260718"
version = "1.0.0-ALPHA-20260804"

repositories {
mavenCentral()
Expand Down
1 change: 1 addition & 0 deletions protocol/osrs-240/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// No-op
40 changes: 40 additions & 0 deletions protocol/osrs-240/osrs-240-api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
dependencies {
implementation(platform(rootProject.libs.netty.bom))
api(rootProject.libs.netty.buffer)
implementation(rootProject.libs.netty.transport)
implementation(rootProject.libs.netty.handler)
implementation(rootProject.libs.netty.native.epoll)
implementation(rootProject.libs.netty.native.kqueue)
implementation(rootProject.libs.netty.iouring)
implementation(rootProject.libs.netty.native.macos.dns.resolver)
val epollClassifiers = listOf("linux-aarch_64", "linux-x86_64", "linux-riscv64")
val kqueueClassifiers = listOf("osx-x86_64")
val iouringClassifiers = listOf("linux-aarch_64", "linux-x86_64")
for (classifier in epollClassifiers) {
implementation(variantOf(rootProject.libs.netty.native.epoll) { classifier(classifier) })
}
for (classifier in kqueueClassifiers) {
implementation(variantOf(rootProject.libs.netty.native.kqueue) { classifier(classifier) })
}
for (classifier in iouringClassifiers) {
implementation(variantOf(rootProject.libs.netty.iouring) { classifier(classifier) })
}
implementation(rootProject.libs.inline.logger)
api(projects.protocol)
api(projects.compression)
api(projects.crypto)
api(projects.protocol.osrs240.osrs240Common)
api(projects.protocol.osrs240.osrs240Model)
implementation(projects.protocol.osrs240.osrs240Internal)
implementation(projects.protocol.osrs240.osrs240Desktop)
implementation(projects.protocol.osrs240.osrs240Shared)
implementation(projects.buffer)
}

mavenPublishing {
pom {
name = "RsProt OSRS 240 API"
description = "The API module for revision 240 OldSchool RuneScape networking, " +
"offering an all-in-one implementation."
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package net.rsprot.protocol.api

import io.netty.channel.ChannelHandlerContext

/**
* The channel exception handler is an interface that is invoked whenever Netty catches
* an exception in the channels. The server is expected to close the connection in any such case,
* if it is still open, and log the exception behind it.
*/
public fun interface ChannelExceptionHandler {
/**
* Invoked whenever a Netty handler catches an exception.
* @param ctx the channel handler context behind this connection
* @param cause the causation behind the exception
*/
public fun exceptionCaught(
ctx: ChannelHandlerContext,
cause: Throwable,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
package net.rsprot.protocol.api

import io.netty.buffer.ByteBufAllocator
import net.rsprot.compression.provider.HuffmanCodecProvider
import net.rsprot.protocol.api.suppliers.NpcInfoSupplier
import net.rsprot.protocol.api.suppliers.PlayerInfoSupplier
import net.rsprot.protocol.api.suppliers.WorldEntityInfoSupplier
import net.rsprot.protocol.common.client.OldSchoolClientType
import net.rsprot.protocol.game.outgoing.codec.npcinfo.DesktopLowResolutionChangeEncoder
import net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo.writer.NpcAvatarExtendedInfoDesktopWriter
import net.rsprot.protocol.game.outgoing.codec.playerinfo.extendedinfo.writer.PlayerAvatarExtendedInfoDesktopWriter
import net.rsprot.protocol.game.outgoing.codec.worldentity.extendedinfo.WorldEntityAvatarExtendedInfoDesktopWriter
import net.rsprot.protocol.game.outgoing.info.npcinfo.DeferredNpcInfoProtocolSupplier
import net.rsprot.protocol.game.outgoing.info.npcinfo.NpcAvatarExtendedInfoWriter
import net.rsprot.protocol.game.outgoing.info.npcinfo.NpcAvatarFactory
import net.rsprot.protocol.game.outgoing.info.npcinfo.NpcAvatarFilter
import net.rsprot.protocol.game.outgoing.info.npcinfo.NpcInfoProtocol
import net.rsprot.protocol.game.outgoing.info.playerinfo.PlayerAvatarExtendedInfoWriter
import net.rsprot.protocol.game.outgoing.info.playerinfo.PlayerAvatarFactory
import net.rsprot.protocol.game.outgoing.info.playerinfo.PlayerInfoProtocol
import net.rsprot.protocol.game.outgoing.info.worldentityinfo.WorldEntityAvatarExtendedInfoWriter
import net.rsprot.protocol.game.outgoing.info.worldentityinfo.WorldEntityAvatarFactory
import net.rsprot.protocol.game.outgoing.info.worldentityinfo.WorldEntityProtocol
import net.rsprot.protocol.internal.client.ClientTypeMap
import net.rsprot.protocol.internal.game.outgoing.info.npcinfo.encoder.NpcResolutionChangeEncoder
import net.rsprot.protocol.internal.game.outgoing.info.util.ZoneIndexStorage

/**
* The entity info protocols class brings together the relatively complex player and NPC info
* protocols. This is responsible for registering all the client types that are used by the user.
* @property playerAvatarFactory the avatar factory for players. Since players have a 1:1 player info
* to avatar ratio, the avatar is automatically included in the player info object that is requested.
* This is additionally strategically placed to improve cache locality and improve the performance
* of the player info protocol.
* @property playerInfoProtocol the main player info protocol responsible for computing the player
* info packet for all the players in the game.
* @property npcAvatarFactory the avatar factory for NPCs. Each NPC must allocate one avatar
* as they spawn, and that avatar must be deallocated when the NPC is fully removed from the game.
* @property npcInfoProtocol the main NPC info protocol responsible for computing the npc info packet
* for all the players in the game.
*/
public class EntityInfoProtocols
private constructor(
public val playerAvatarFactory: PlayerAvatarFactory,
public val playerInfoProtocol: PlayerInfoProtocol,
public val npcAvatarFactory: NpcAvatarFactory,
public val npcInfoProtocol: NpcInfoProtocol,
public val worldEntityAvatarFactory: WorldEntityAvatarFactory,
public val worldEntityInfoProtocol: WorldEntityProtocol,
) {
internal companion object {
/**
* Initializes the player and NPC info avatar factories and protocols.
* @param allocator the byte buffer allocator used for player and NPC info main buffers,
* as well as any pre-computed extended info blocks.
* @param clientTypes the list of client types to register
* @param huffmanCodecProvider the Huffman codec provider that will be used to compute
* the chat extended info block, any others in the future that may require it.
* @param playerInfoSupplier the class wrapping the worker used to perform computations,
* as well as the filter for extended info blocks that ensures that the packet does not
* under any circumstances exceed the maximum packet limitations.
* @param npcInfoSupplier the class wrapping the worker used to perform computations,
* as well as the filter for extended info blocks that ensures that the packet does not
* under any circumstances exceed the maximum packet limitations. Furthermore, unlike
* player info, this will also provide an implementation for exceptions caught during
* pre-computations of NPC avatars. It is up to the server to decide how to handle
* any exceptions which are caught when computing information for avatars. The least
* destructive way is to remove the underlying NPC from the world when that happens,
* and log the exception in the process. This will still cause any observers to disconnect,
* however, but it ensures that anyone else that comes around the same area will not
* experience the same fate. There is also an implementation that is used to supply
* indices of nearby NPCs for the NPC info packet from the server's perspective,
* given a number of arguments necessary to determine it. The server is expected
* to return an iterator of all the indices of the NPCs that match the predicate,
* even if a NPC is already tracked by a given player. The protocol is responsible
* for ensuring no duplications will occur.
* @return a class wrapping all the protocols into one object.
*/
fun initialize(
allocator: ByteBufAllocator,
clientTypes: List<OldSchoolClientType>,
huffmanCodecProvider: HuffmanCodecProvider,
playerInfoSupplier: PlayerInfoSupplier,
npcInfoSupplier: NpcInfoSupplier,
worldEntityInfoSupplier: WorldEntityInfoSupplier,
filter: NpcAvatarFilter?,
): EntityInfoProtocols {
val playerWriters = mutableListOf<PlayerAvatarExtendedInfoWriter>()
val npcWriters = mutableListOf<NpcAvatarExtendedInfoWriter>()
val npcResolutionChangeEncoders = mutableListOf<NpcResolutionChangeEncoder>()
val worldEntityWriters = mutableListOf<WorldEntityAvatarExtendedInfoWriter>()
if (OldSchoolClientType.DESKTOP in clientTypes) {
playerWriters += PlayerAvatarExtendedInfoDesktopWriter()
npcWriters += NpcAvatarExtendedInfoDesktopWriter()
npcResolutionChangeEncoders += DesktopLowResolutionChangeEncoder()
worldEntityWriters += WorldEntityAvatarExtendedInfoDesktopWriter()
}
val zoneIndexStorage =
ZoneIndexStorage(
ZoneIndexStorage.WORLDENTITY_CAPACITY,
)
val worldEntityAvatarFactory =
buildWorldEntityAvatarFactory(
allocator,
zoneIndexStorage,
worldEntityWriters,
huffmanCodecProvider,
)
val worldEntityProtocol =
buildWorldEntityInfoProtocol(
allocator,
worldEntityInfoSupplier,
worldEntityAvatarFactory,
zoneIndexStorage,
)
val playerAvatarFactory =
buildPlayerAvatarFactory(allocator, playerInfoSupplier, playerWriters, huffmanCodecProvider)
val playerInfoProtocol =
buildPlayerInfoProtocol(
allocator,
playerInfoSupplier,
playerAvatarFactory,
)
val storage =
ZoneIndexStorage(
ZoneIndexStorage.NPC_CAPACITY,
)
val supplier = DeferredNpcInfoProtocolSupplier()
val npcAvatarFactory =
buildNpcAvatarFactory(
allocator,
npcInfoSupplier,
npcWriters,
huffmanCodecProvider,
storage,
supplier,
)
val npcInfoProtocol =
buildNpcInfoProtocol(
allocator,
npcInfoSupplier,
npcResolutionChangeEncoders,
npcAvatarFactory,
storage,
filter,
)
supplier.supply(npcInfoProtocol)

return EntityInfoProtocols(
playerAvatarFactory,
playerInfoProtocol,
npcAvatarFactory,
npcInfoProtocol,
worldEntityAvatarFactory,
worldEntityProtocol,
)
}

private fun buildNpcInfoProtocol(
allocator: ByteBufAllocator,
npcInfoSupplier: NpcInfoSupplier,
npcResolutionChangeEncoders: MutableList<NpcResolutionChangeEncoder>,
npcAvatarFactory: NpcAvatarFactory,
zoneIndexStorage: ZoneIndexStorage,
filter: NpcAvatarFilter?,
) = NpcInfoProtocol(
allocator,
ClientTypeMap.of(
npcResolutionChangeEncoders,
OldSchoolClientType.COUNT,
) {
it.clientType
},
npcAvatarFactory,
npcInfoSupplier.npcAvatarExceptionHandler,
npcInfoSupplier.npcInfoProtocolWorker,
zoneIndexStorage,
filter,
)

private fun buildNpcAvatarFactory(
allocator: ByteBufAllocator,
npcInfoSupplier: NpcInfoSupplier,
npcWriters: MutableList<NpcAvatarExtendedInfoWriter>,
huffmanCodecProvider: HuffmanCodecProvider,
zoneIndexStorage: ZoneIndexStorage,
npcInfoProtocolSupplier: DeferredNpcInfoProtocolSupplier,
): NpcAvatarFactory =
NpcAvatarFactory(
allocator,
npcInfoSupplier.npcExtendedInfoFilter,
npcWriters,
huffmanCodecProvider,
zoneIndexStorage,
npcInfoProtocolSupplier,
)

private fun buildWorldEntityAvatarFactory(
allocator: ByteBufAllocator,
zoneIndexStorage: ZoneIndexStorage,
worldEntityWriters: MutableList<WorldEntityAvatarExtendedInfoWriter>,
huffmanCodecProvider: HuffmanCodecProvider,
): WorldEntityAvatarFactory =
WorldEntityAvatarFactory(
allocator,
zoneIndexStorage,
worldEntityWriters,
huffmanCodecProvider,
)

private fun buildWorldEntityInfoProtocol(
allocator: ByteBufAllocator,
worldEntityInfoSupplier: WorldEntityInfoSupplier,
worldEntityAvatarFactory: WorldEntityAvatarFactory,
zoneIndexStorage: ZoneIndexStorage,
) = WorldEntityProtocol(
allocator,
worldEntityInfoSupplier.worldEntityAvatarExceptionHandler,
worldEntityAvatarFactory,
worldEntityInfoSupplier.worldEntityInfoProtocolWorker,
zoneIndexStorage,
)

private fun buildPlayerInfoProtocol(
allocator: ByteBufAllocator,
playerInfoSupplier: PlayerInfoSupplier,
playerAvatarFactory: PlayerAvatarFactory,
): PlayerInfoProtocol =
PlayerInfoProtocol(
allocator,
playerInfoSupplier.playerInfoProtocolWorker,
playerAvatarFactory,
)

private fun buildPlayerAvatarFactory(
allocator: ByteBufAllocator,
playerInfoSupplier: PlayerInfoSupplier,
playerWriters: MutableList<PlayerAvatarExtendedInfoWriter>,
huffmanCodecProvider: HuffmanCodecProvider,
): PlayerAvatarFactory =
PlayerAvatarFactory(
allocator,
playerInfoSupplier.playerExtendedInfoFilter,
playerWriters,
huffmanCodecProvider,
)
}
}
Loading
Loading