Skip to content

Commit

Permalink
Revision 228 (#53)
Browse files Browse the repository at this point in the history
* feat: revision 228 (initial copy of 227)

* refactor: update game server prot ids

* refactor: update game client prot ids

* feat: client prots

* feat: all but info packets

* feat: player info packet

* feat: npc info packet

* feat: login crc

* fix: revision 228 tests
  • Loading branch information
Z-Kris authored Jan 15, 2025
1 parent 7900e6f commit 9549ea6
Show file tree
Hide file tree
Showing 741 changed files with 53,246 additions and 0 deletions.
1 change: 1 addition & 0 deletions protocol/osrs-228/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-228/osrs-228-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.incubator.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.incubator.iouring) { classifier(classifier) })
}
implementation(rootProject.libs.inline.logger)
api(projects.protocol)
api(projects.compression)
api(projects.crypto)
api(projects.protocol.osrs228.osrs228Common)
api(projects.protocol.osrs228.osrs228Model)
implementation(projects.protocol.osrs228.osrs228Internal)
implementation(projects.protocol.osrs228.osrs228Desktop)
implementation(projects.protocol.osrs228.osrs228Shared)
implementation(projects.buffer)
}

mavenPublishing {
pom {
name = "RsProt OSRS 228 API"
description = "The API module for revision 228 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,227 @@
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.ClientTypeMap
import net.rsprot.protocol.common.client.OldSchoolClientType
import net.rsprot.protocol.common.game.outgoing.info.npcinfo.encoder.NpcResolutionChangeEncoder
import net.rsprot.protocol.common.game.outgoing.info.util.ZoneIndexStorage
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.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.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.WorldEntityAvatarFactory
import net.rsprot.protocol.game.outgoing.info.worldentityinfo.WorldEntityProtocol

/**
* 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,
): EntityInfoProtocols {
val playerWriters = mutableListOf<PlayerAvatarExtendedInfoWriter>()
val npcWriters = mutableListOf<NpcAvatarExtendedInfoWriter>()
val npcResolutionChangeEncoders = mutableListOf<NpcResolutionChangeEncoder>()
if (OldSchoolClientType.DESKTOP in clientTypes) {
playerWriters += PlayerAvatarExtendedInfoDesktopWriter()
npcWriters += NpcAvatarExtendedInfoDesktopWriter()
npcResolutionChangeEncoders += DesktopLowResolutionChangeEncoder()
}
val zoneIndexStorage = ZoneIndexStorage(ZoneIndexStorage.WORLDENTITY_CAPACITY)
val worldEntityAvatarFactory =
buildWorldEntityAvatarFactory(
allocator,
zoneIndexStorage,
)
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,
)
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,
) = NpcInfoProtocol(
allocator,
ClientTypeMap.of(
npcResolutionChangeEncoders,
OldSchoolClientType.COUNT,
) {
it.clientType
},
npcAvatarFactory,
npcInfoSupplier.npcAvatarExceptionHandler,
npcInfoSupplier.npcInfoProtocolWorker,
zoneIndexStorage,
)

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,
): WorldEntityAvatarFactory =
WorldEntityAvatarFactory(
allocator,
zoneIndexStorage,
)

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,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package net.rsprot.protocol.api

import net.rsprot.crypto.xtea.XteaKey
import net.rsprot.protocol.api.login.GameLoginResponseHandler
import net.rsprot.protocol.loginprot.incoming.util.AuthenticationType
import net.rsprot.protocol.loginprot.incoming.util.LoginBlock

/**
* A handler interface for any game logins and reconnections.
* @param R the receiver of the incoming game packets, typically a Player class.
*/
public interface GameConnectionHandler<R> {
/**
* The onLogin function is triggered whenever a login request is received by the library,
* and it passes all the initial validation necessary. The server is responsible
* for doing most of the validation here, but preliminary things like max number of connections
* and session ids will have been pre-checked by us.
* @param responseHandler the handler used to write a successful or failed login response,
* depending on the decisions made by the server.
* @param block the login block sent by the client, containing all the information the server
* will need.
*/
public fun onLogin(
responseHandler: GameLoginResponseHandler<R>,
block: LoginBlock<AuthenticationType<*>>,
)

/**
* The onReconnect function is triggered whenever a reconnect request is received
* by the library. It is worth noting that Proof of Work will not be involved
* if this is the case, assuming it is enabled in the first place.
* Instead of transmitting the password, the client will transmit the seed used
* by the previous login connection. If the seed does not match with what the
* server knows, the request should be rejected. If the reconnect is successful,
* the server should replace the Session object in that player with the one
* provided by the response handler. The old session will close or time out shortly
* afterwards, if it already hasn't.
* @param responseHandler the handler used to write a successful or failed reconnect response,
* depending on the decisions made by the server.
* @param block the login block sent by the client, containing all the information the server
* will need.
*/
public fun onReconnect(
responseHandler: GameLoginResponseHandler<R>,
block: LoginBlock<XteaKey>,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package net.rsprot.protocol.api

import net.rsprot.protocol.ClientProtCategory

/**
* An interface for tracking incoming game messages, in order to avoid
* decoding and consuming too many messages if the client is flooding us
* with them.
* This implementation must be thread safe in the sense that the
* increment and reset functions could be called concurrently from different
* threads. The default implementation uses an array for tracking the counts
* and thus does not need such thread safety here.
*/
public interface GameMessageCounter {
/**
* Increments the message counter for the provided client prot
* category.
* @param clientProtCategory the category of the incoming packet.
*/
public fun increment(clientProtCategory: ClientProtCategory)

/**
* Whether any of the message categories have reached their limit
* for maximum number of decoded messages.
*/
public fun isFull(): Boolean

/**
* Resets the tracked counts for the messages.
*/
public fun reset()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package net.rsprot.protocol.api

/**
* Gets the message counter provider for incoming game messages.
* This is in a provider implementation as one instance is allocated
* for each session object.
*/
public fun interface GameMessageCounterProvider {
/**
* Provides a game message counter implementation.
* A new instance must be allocated with each request,
* as this is per session basis.
*/
public fun provide(): GameMessageCounter
}
Loading

0 comments on commit 9549ea6

Please sign in to comment.