Skip to content

Commit

Permalink
feat: friend chat packets
Browse files Browse the repository at this point in the history
  • Loading branch information
Z-Kris committed Apr 10, 2024
1 parent d44dbf7 commit 769b858
Show file tree
Hide file tree
Showing 12 changed files with 649 additions and 2 deletions.
3 changes: 2 additions & 1 deletion .idea/dictionaries/krist.xml

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

50 changes: 50 additions & 0 deletions compression/src/main/kotlin/net/rsprot/compression/Base37.kt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@file:Suppress("DuplicatedCode")

package net.rsprot.compression

/**
Expand All @@ -6,6 +8,7 @@ package net.rsprot.compression
public data object Base37 {
private const val BASE_37: Long = 37
private const val MAXIMUM_POSSIBLE_12_CHARACTER_VALUE: Long = 6582952005840035280L
private const val NBSP: Int = 160
private val ALPHABET: CharArray =
charArrayOf(
'_',
Expand Down Expand Up @@ -133,4 +136,51 @@ public data object Base37 {
.reverse()
.toString()
}

/**
* Decodes a base-37 encoded long into the respective string,
* replacing all underscores with spaces, as well as all first
* letters of each individual word to begin with an uppercase
* letter.
* If the input long is within the correct range, but isn't v
* @param encoded the base-37 encoded long value.
* @return the string that was encoded in base-37 encoding.
* @throws IllegalArgumentException if the encoded value exceeds
* the maximum 12-character long value, or if the value
* isn't in base-37 representation.
*/
public fun decodeWithCase(encoded: Long): String {
if (encoded == 0L) {
return ""
}
require(encoded in 0..MAXIMUM_POSSIBLE_12_CHARACTER_VALUE) {
"Invalid encoded value: $encoded"
}
require(encoded % BASE_37 != 0L) {
"Encoded value not in base-37: $encoded"
}
var length = 0
var lengthCounter = encoded
while (lengthCounter != 0L) {
++length
lengthCounter /= BASE_37
}
val builder = StringBuilder(length)
var rem = encoded
while (rem != 0L) {
val var6 = rem
rem /= BASE_37
var char = ALPHABET[(var6 - rem * BASE_37).toInt()]
if (char == '_') {
val lastIndex = builder.length - 1
builder.setCharAt(lastIndex, builder[lastIndex].uppercaseChar())
char = NBSP.toChar()
}
builder.append(char)
}

builder.reverse()
builder.setCharAt(0, builder[0].uppercaseChar())
return builder.toString()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package net.rsprot.protocol.game.outgoing.codec.friendchat

import io.netty.channel.ChannelHandlerContext
import net.rsprot.buffer.JagByteBuf
import net.rsprot.protocol.ServerProt
import net.rsprot.protocol.channel.ChannelAttributes
import net.rsprot.protocol.game.outgoing.friendchat.MessageFriendChannel
import net.rsprot.protocol.game.outgoing.prot.GameServerProt
import net.rsprot.protocol.message.codec.MessageEncoder
import net.rsprot.protocol.metadata.Consistent

@Consistent
public class MessageFriendChannelEncoder : MessageEncoder<MessageFriendChannel> {
override val prot: ServerProt = GameServerProt.MESSAGE_FRIENDCHANNEL

override fun encode(
ctx: ChannelHandlerContext,
buffer: JagByteBuf,
message: MessageFriendChannel,
) {
val huffmanCodec =
ctx.channel().attr(ChannelAttributes.HUFFMAN_CODEC).get()
?: throw IllegalStateException("Huffman codec not initialized.")
buffer.pjstr(message.sender)
buffer.p8(message.channelNameBase37)
buffer.p2(message.worldId)
buffer.p3(message.worldMessageCounter)
buffer.p1(message.chatCrownType)
huffmanCodec.encode(buffer, message.message)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package net.rsprot.protocol.game.outgoing.codec.friendchat

import io.netty.channel.ChannelHandlerContext
import net.rsprot.buffer.JagByteBuf
import net.rsprot.protocol.ServerProt
import net.rsprot.protocol.game.outgoing.friendchat.UpdateFriendChatChannelFullV1
import net.rsprot.protocol.game.outgoing.prot.GameServerProt
import net.rsprot.protocol.message.codec.MessageEncoder
import net.rsprot.protocol.metadata.Consistent

@Consistent
public class UpdateFriendChatChannelFullV1Encoder : MessageEncoder<UpdateFriendChatChannelFullV1> {
override val prot: ServerProt = GameServerProt.UPDATE_FRIENDCHAT_CHANNEL_FULL_V1

override fun encode(
ctx: ChannelHandlerContext,
buffer: JagByteBuf,
message: UpdateFriendChatChannelFullV1,
) {
buffer.pjstr(message.channelOwner)
buffer.p8(message.channelNameBase37)
buffer.p1(message.kickRank)
buffer.p1(message.entries.size)
for (entry in message.entries) {
buffer.pjstr(entry.name)
buffer.p2(entry.worldId)
buffer.p1(entry.rank)
buffer.pjstr(entry.worldName)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package net.rsprot.protocol.game.outgoing.codec.friendchat

import io.netty.channel.ChannelHandlerContext
import net.rsprot.buffer.JagByteBuf
import net.rsprot.protocol.ServerProt
import net.rsprot.protocol.game.outgoing.friendchat.UpdateFriendChatChannelFullV2
import net.rsprot.protocol.game.outgoing.prot.GameServerProt
import net.rsprot.protocol.message.codec.MessageEncoder
import net.rsprot.protocol.metadata.Consistent

@Consistent
public class UpdateFriendChatChannelFullV2Encoder : MessageEncoder<UpdateFriendChatChannelFullV2> {
override val prot: ServerProt = GameServerProt.UPDATE_FRIENDCHAT_CHANNEL_FULL_V2

override fun encode(
ctx: ChannelHandlerContext,
buffer: JagByteBuf,
message: UpdateFriendChatChannelFullV2,
) {
buffer.pjstr(message.channelOwner)
buffer.p8(message.channelNameBase37)
buffer.p1(message.kickRank)
buffer.pSmart1or2(message.entries.size)
for (entry in message.entries) {
buffer.pjstr(entry.name)
buffer.p2(entry.worldId)
buffer.p1(entry.rank)
buffer.pjstr(entry.worldName)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package net.rsprot.protocol.game.outgoing.codec.friendchat

import io.netty.channel.ChannelHandlerContext
import net.rsprot.buffer.JagByteBuf
import net.rsprot.protocol.ServerProt
import net.rsprot.protocol.game.outgoing.friendchat.UpdateFriendChatChannelSingleUser
import net.rsprot.protocol.game.outgoing.prot.GameServerProt
import net.rsprot.protocol.message.codec.MessageEncoder
import net.rsprot.protocol.metadata.Consistent

@Consistent
public class UpdateFriendChatChannelSingleUserEncoder : MessageEncoder<UpdateFriendChatChannelSingleUser> {
override val prot: ServerProt = GameServerProt.UPDATE_FRIENDCHAT_CHANNEL_SINGLEUSER

override fun encode(
ctx: ChannelHandlerContext,
buffer: JagByteBuf,
message: UpdateFriendChatChannelSingleUser,
) {
val user = message.user
buffer.pjstr(user.name)
buffer.p2(user.worldId)
buffer.p1(user.rank)
if (user is UpdateFriendChatChannelSingleUser.AddedFriendChatUser) {
buffer.pjstr(user.worldName)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package net.rsprot.protocol.game.outgoing.friendchat

import net.rsprot.compression.Base37
import net.rsprot.protocol.message.OutgoingMessage

/**
* Message friendchannel is used to transmit messages within a friend
* chat channel.
* @property sender the name of the player who is sending the message
* @property channelName the name of the friend chat channel
* @property worldMessageCounter the world-local message counter.
* Each world must have its own message counter which is used to create
* a unique id for each message. This message counter must be
* incrementing with each message that is sent out.
* If two messages share the same unique id (which is a combination of
* the [worldId] and the [worldMessageCounter] properties),
* the client will not render the second message if it already has one
* received in the last 100 messages.
* It is additionally worth noting that servers with low population
* should probably not start the counter at the same value with each
* game boot, as the probability of multiple messages coinciding
* is relatively high in that scenario, given the low quantity of
* messages sent out to begin with.
* Additionally, only the first 24 bits of the counter are utilized,
* meaning a value from 0 to 16,777,215 (inclusive).
* A good starting point for message counting would be to take the
* hour of the year and multiply it by 50,000 when the server boots
* up. This means the roll-over happens roughly after every two weeks.
* Fine-tuning may be used to make it more granular, but the overall
* idea remains the same.
* @property chatCrownType the id of the crown to render next to the
* name of the sender.
* @property message the message to be sent in the friend chat
* channel.
*/
public class MessageFriendChannel private constructor(
public val sender: String,
public val channelNameBase37: Long,
private val _worldId: UShort,
public val worldMessageCounter: Int,
private val _chatCrownType: UByte,
public val message: String,
) : OutgoingMessage {
public constructor(
sender: String,
channelName: String,
worldId: Int,
worldMessageCounter: Int,
chatCrownType: Int,
message: String,
) : this(
sender,
Base37.encode(channelName),
worldId.toUShort(),
worldMessageCounter,
chatCrownType.toUByte(),
message,
)

public val channelName: String
get() = Base37.decodeWithCase(channelNameBase37)
public val worldId: Int
get() = _worldId.toInt()
public val chatCrownType: Int
get() = _chatCrownType.toInt()

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

other as MessageFriendChannel

if (sender != other.sender) return false
if (channelNameBase37 != other.channelNameBase37) return false
if (_worldId != other._worldId) return false
if (worldMessageCounter != other.worldMessageCounter) return false
if (_chatCrownType != other._chatCrownType) return false
if (message != other.message) return false

return true
}

override fun hashCode(): Int {
var result = sender.hashCode()
result = 31 * result + channelNameBase37.hashCode()
result = 31 * result + _worldId.hashCode()
result = 31 * result + worldMessageCounter
result = 31 * result + _chatCrownType.hashCode()
result = 31 * result + message.hashCode()
return result
}

override fun toString(): String {
return "MessageFriendChannel(" +
"sender='$sender', " +
"channelName='$channelName', " +
"worldId=$worldId, " +
"worldMessageCounter=$worldMessageCounter, " +
"chatCrownType=$chatCrownType, " +
"message='$message'" +
")"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package net.rsprot.protocol.game.outgoing.friendchat

public sealed class UpdateFriendChatChannelFull {
public abstract val channelOwner: String
public abstract val channelName: String
public abstract val kickRank: Int
public abstract val entries: List<FriendChatEntry>

/**
* A class to contain all the properties of a player in a friend chat.
* @property name the name of the player that is in the friend chat
* @property worldId the id of the world in which the given user is
* @property rank the rank of the given used in this friend chat
* @property worldName world name, unused in OldSchool RuneScape.
*/
public class FriendChatEntry private constructor(
public val name: String,
private val _worldId: UShort,
private val _rank: Byte,
public val worldName: String,
) {
public constructor(
name: String,
worldId: Int,
rank: Int,
string: String,
) : this(
name,
worldId.toUShort(),
rank.toByte(),
string,
)

public val worldId: Int
get() = _worldId.toInt()
public val rank: Int
get() = _rank.toInt()

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

other as FriendChatEntry

if (name != other.name) return false
if (_worldId != other._worldId) return false
if (_rank != other._rank) return false
if (worldName != other.worldName) return false

return true
}

override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + _worldId.hashCode()
result = 31 * result + _rank.hashCode()
result = 31 * result + worldName.hashCode()
return result
}

override fun toString(): String {
return "FriendChatEntry(" +
"name='$name', " +
"worldId=$worldId, " +
"rank=$rank, " +
"worldName='$worldName'" +
")"
}
}
}
Loading

0 comments on commit 769b858

Please sign in to comment.