mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 06:28:28 +08:00
Block server now sends player to a lobby.
This commit is contained in:
parent
21e9ebaaee
commit
e6abad4f09
@ -51,7 +51,7 @@ expect class Buffer {
|
||||
fun getFloat(offset: Int): Float
|
||||
|
||||
/**
|
||||
* Reads a ASCII-encoded string at the given offset.
|
||||
* Reads an ASCII-encoded string at the given offset.
|
||||
*/
|
||||
fun getStringAscii(offset: Int, maxByteLength: Int, nullTerminated: Boolean): String
|
||||
|
||||
@ -100,6 +100,18 @@ expect class Buffer {
|
||||
*/
|
||||
fun setFloat(offset: Int, value: Float): Buffer
|
||||
|
||||
/**
|
||||
* Writes an ASCII-encoded string at the given offset. If [str] is shorter than [byteLength],
|
||||
* nul bytes will be inserted until [byteLength] bytes have been written.
|
||||
*/
|
||||
fun setStringAscii(offset: Int, str: String, byteLength: Int): Buffer
|
||||
|
||||
/**
|
||||
* Writes a UTF-16-encoded string at the given offset. If less than [byteLength] bytes can be
|
||||
* written this way, nul bytes will be inserted until [byteLength] bytes have been written.
|
||||
*/
|
||||
fun setStringUtf16(offset: Int, str: String, byteLength: Int): Buffer
|
||||
|
||||
/**
|
||||
* Writes 0 bytes to the entire buffer.
|
||||
*/
|
||||
|
@ -6,6 +6,7 @@ import org.khronos.webgl.Int8Array
|
||||
import org.khronos.webgl.Uint8Array
|
||||
import org.w3c.dom.WindowOrWorkerGlobalScope
|
||||
import world.phantasmal.psolib.Endianness
|
||||
import kotlin.math.min
|
||||
|
||||
external val self: WindowOrWorkerGlobalScope
|
||||
|
||||
@ -156,6 +157,36 @@ actual class Buffer private constructor(
|
||||
return this
|
||||
}
|
||||
|
||||
actual fun setStringAscii(offset: Int, str: String, byteLength: Int): Buffer {
|
||||
checkOffset(offset, byteLength)
|
||||
|
||||
for (i in 0 until min(str.length, byteLength)) {
|
||||
val codePoint = str[i].code.toByte()
|
||||
dataView.setInt8(offset + i, codePoint)
|
||||
}
|
||||
|
||||
for (i in str.length until byteLength) {
|
||||
dataView.setInt8(offset + i, 0)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
actual fun setStringUtf16(offset: Int, str: String, byteLength: Int): Buffer {
|
||||
checkOffset(offset, byteLength)
|
||||
|
||||
for (i in 0 until min(str.length, byteLength / 2)) {
|
||||
val codePoint = str[i].code.toShort()
|
||||
dataView.setInt16(offset + 2 * i, codePoint)
|
||||
}
|
||||
|
||||
for (i in 2 * str.length until byteLength) {
|
||||
dataView.setInt8(offset + i, 0)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
actual fun zero(): Buffer =
|
||||
fillByte(0)
|
||||
|
||||
|
@ -4,6 +4,7 @@ import world.phantasmal.psolib.Endianness
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.util.*
|
||||
import kotlin.math.min
|
||||
|
||||
actual class Buffer private constructor(
|
||||
private var buf: ByteBuffer,
|
||||
@ -157,6 +158,36 @@ actual class Buffer private constructor(
|
||||
return this
|
||||
}
|
||||
|
||||
actual fun setStringAscii(offset: Int, str: String, byteLength: Int): Buffer {
|
||||
checkOffset(offset, byteLength)
|
||||
|
||||
for (i in 0 until min(str.length, byteLength)) {
|
||||
val codePoint = str[i].code.toByte()
|
||||
buf.put(offset + i, codePoint)
|
||||
}
|
||||
|
||||
for (i in str.length until byteLength) {
|
||||
buf.put(offset + i, 0)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
actual fun setStringUtf16(offset: Int, str: String, byteLength: Int): Buffer {
|
||||
checkOffset(offset, byteLength)
|
||||
|
||||
for (i in 0 until min(str.length, byteLength / 2)) {
|
||||
val codePoint = str[i].code.toShort()
|
||||
buf.putShort(offset + 2 * i, codePoint)
|
||||
}
|
||||
|
||||
for (i in 2 * str.length until byteLength) {
|
||||
buf.putShort(offset + i, 0)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
actual fun zero(): Buffer =
|
||||
fillByte(0)
|
||||
|
||||
|
@ -221,7 +221,7 @@ private fun initialize(config: Config): PhantasmalServer {
|
||||
)
|
||||
}
|
||||
|
||||
for (block in blocks.values) {
|
||||
for ((index, block) in blocks.values.withIndex()) {
|
||||
LOGGER.info {
|
||||
"""Configuring block server ${block.name} ("${block.uiName}") to bind to ${block.bindPair}."""
|
||||
}
|
||||
@ -230,7 +230,7 @@ private fun initialize(config: Config): PhantasmalServer {
|
||||
BlockServer(
|
||||
block.name,
|
||||
block.bindPair,
|
||||
block.uiName,
|
||||
blockNo = index + 1,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -11,6 +11,8 @@ private const val KEY_SIZE: Int = 48
|
||||
const val BB_HEADER_SIZE: Int = 8
|
||||
const val BB_MSG_SIZE_POS: Int = 0
|
||||
const val BB_MSG_CODE_POS: Int = 2
|
||||
const val BB_MSG_FLAGS_POS: Int = 4
|
||||
const val BB_MSG_BODY_POS: Int = 8
|
||||
|
||||
object BbMessageDescriptor : MessageDescriptor<BbMessage> {
|
||||
override val headerSize: Int = BB_HEADER_SIZE
|
||||
@ -18,8 +20,8 @@ object BbMessageDescriptor : MessageDescriptor<BbMessage> {
|
||||
override fun readHeader(buffer: Buffer): Header {
|
||||
val size = buffer.getUShort(BB_MSG_SIZE_POS).toInt()
|
||||
val code = buffer.getUShort(BB_MSG_CODE_POS).toInt()
|
||||
// Ignore 4 flag bytes.
|
||||
return Header(code, size)
|
||||
val flags = buffer.getInt(BB_MSG_FLAGS_POS)
|
||||
return Header(code, size, flags)
|
||||
}
|
||||
|
||||
override fun readMessage(buffer: Buffer): BbMessage =
|
||||
@ -30,8 +32,11 @@ object BbMessageDescriptor : MessageDescriptor<BbMessage> {
|
||||
0x0007 -> BbMessage.BlockList(buffer)
|
||||
0x0010 -> BbMessage.MenuSelect(buffer)
|
||||
0x0019 -> BbMessage.Redirect(buffer)
|
||||
0x0061 -> BbMessage.CharData(buffer)
|
||||
0x0067 -> BbMessage.JoinLobby(buffer)
|
||||
0x0083 -> BbMessage.LobbyList(buffer)
|
||||
0x0093 -> BbMessage.Authenticate(buffer)
|
||||
0x0095 -> BbMessage.GetCharData(buffer)
|
||||
0x00A0 -> BbMessage.ShipList(buffer)
|
||||
0x01DC -> BbMessage.GuildCardHeader(buffer)
|
||||
0x02DC -> BbMessage.GuildCardChunk(buffer)
|
||||
@ -40,8 +45,9 @@ object BbMessageDescriptor : MessageDescriptor<BbMessage> {
|
||||
0x00E2 -> BbMessage.Account(buffer)
|
||||
0x00E3 -> BbMessage.CharSelect(buffer)
|
||||
0x00E4 -> BbMessage.CharSelectAck(buffer)
|
||||
0x00E5 -> BbMessage.CharData(buffer)
|
||||
0x00E5 -> BbMessage.Char(buffer)
|
||||
0x00E6 -> BbMessage.AuthData(buffer)
|
||||
0x00E7 -> BbMessage.FullCharacterData(buffer)
|
||||
0x01E8 -> BbMessage.Checksum(buffer)
|
||||
0x02E8 -> BbMessage.ChecksumAck(buffer)
|
||||
0x03E8 -> BbMessage.GetGuildCardHeader(buffer)
|
||||
@ -56,6 +62,7 @@ object BbMessageDescriptor : MessageDescriptor<BbMessage> {
|
||||
sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_SIZE) {
|
||||
override val code: Int get() = buffer.getUShort(BB_MSG_CODE_POS).toInt()
|
||||
override val size: Int get() = buffer.getUShort(BB_MSG_SIZE_POS).toInt()
|
||||
override val flags: Int get() = buffer.getInt(BB_MSG_FLAGS_POS)
|
||||
|
||||
// 0x0003
|
||||
class InitEncryption(buffer: Buffer) : BbMessage(buffer), InitEncryptionMessage {
|
||||
@ -126,20 +133,16 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_
|
||||
require(value.size == 4)
|
||||
setByteArray(0, value)
|
||||
}
|
||||
override var port: Int
|
||||
get() = uShort(4).toInt()
|
||||
set(value) {
|
||||
require(value in 0..65535)
|
||||
setShort(4, value.toShort())
|
||||
}
|
||||
override var port: UShort
|
||||
get() = uShort(4)
|
||||
set(value) = setUShort(4, value)
|
||||
|
||||
constructor(ipAddress: ByteArray, port: Int) : this(
|
||||
constructor(ipAddress: ByteArray, port: UShort) : this(
|
||||
buf(0x0019, 8) {
|
||||
require(ipAddress.size == 4)
|
||||
require(port in 0..65535)
|
||||
|
||||
writeByteArray(ipAddress)
|
||||
writeShort(port.toShort())
|
||||
writeUShort(port)
|
||||
writeShort(0) // Padding.
|
||||
}
|
||||
)
|
||||
@ -151,6 +154,55 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_
|
||||
)
|
||||
}
|
||||
|
||||
// 0x0061
|
||||
class CharData(buffer: Buffer) : BbMessage(buffer)
|
||||
|
||||
// 0x0067
|
||||
class JoinLobby(buffer: Buffer) : BbMessage(buffer) {
|
||||
val playerCount: Int get() = flags
|
||||
var clientId: UByte
|
||||
get() = uByte(0)
|
||||
set(value) = setUByte(0, value)
|
||||
var leaderId: UByte
|
||||
get() = uByte(1)
|
||||
set(value) = setUByte(1, value)
|
||||
var lobbyNo: UByte
|
||||
get() = uByte(3)
|
||||
set(value) = setUByte(3, value)
|
||||
var blockNo: UShort
|
||||
get() = uShort(4)
|
||||
set(value) = setUShort(4, value)
|
||||
|
||||
constructor(
|
||||
clientId: UByte,
|
||||
leaderId: UByte,
|
||||
disableUdp: Boolean,
|
||||
lobbyNo: UByte,
|
||||
blockNo: UShort,
|
||||
event: UShort,
|
||||
) : this(
|
||||
buf(0x0067, 12 + 1312, flags = 1) { // TODO: Set flags to player count.
|
||||
writeUByte(clientId)
|
||||
writeUByte(leaderId)
|
||||
writeByte(if (disableUdp) 1 else 0)
|
||||
writeUByte(lobbyNo)
|
||||
writeUShort(blockNo)
|
||||
writeUShort(event)
|
||||
writeInt(0) // Unused.
|
||||
repeat(328) { writeInt(0) }
|
||||
}
|
||||
)
|
||||
|
||||
override fun toString(): String =
|
||||
messageString(
|
||||
"playerCount" to playerCount,
|
||||
"clientId" to clientId,
|
||||
"leaderId" to leaderId,
|
||||
"lobbyNo" to lobbyNo,
|
||||
"blockNo" to blockNo,
|
||||
)
|
||||
}
|
||||
|
||||
// 0x0083
|
||||
class LobbyList(buffer: Buffer) : BbMessage(buffer) {
|
||||
constructor() : this(
|
||||
@ -174,17 +226,15 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_
|
||||
val guildCard: Int get() = int(4)
|
||||
val version: Short get() = short(8)
|
||||
val teamId: Int get() = int(16)
|
||||
val userName: String
|
||||
get() = stringAscii(offset = 20, maxByteLength = 16, nullTerminated = true)
|
||||
val password: String
|
||||
get() = stringAscii(offset = 68, maxByteLength = 16, nullTerminated = true)
|
||||
val userName: String get() = stringAscii(offset = 20, maxByteLength = 16)
|
||||
val password: String get() = stringAscii(offset = 68, maxByteLength = 16)
|
||||
val magic: Int get() = int(132) // Should be 0xDEADBEEF
|
||||
val charSlot: Int get() = byte(136).toInt()
|
||||
val charSelected: Boolean get() = byte(137).toInt() != 0
|
||||
}
|
||||
|
||||
// 0x0095
|
||||
class GetCharacterInfo(buffer: Buffer) : BbMessage(buffer) {
|
||||
class GetCharData(buffer: Buffer) : BbMessage(buffer) {
|
||||
constructor() : this(buf(0x0095))
|
||||
}
|
||||
|
||||
@ -259,7 +309,8 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_
|
||||
buf(0x00E2, 2804) {
|
||||
// 276 Bytes of unknown data.
|
||||
repeat(69) { writeInt(0) }
|
||||
writeByteArray(DEFAULT_KEYBOARD_GAMEPAD_CONFIG)
|
||||
writeByteArray(DEFAULT_KEYBOARD_CONFIG)
|
||||
writeByteArray(DEFAULT_GAMEPAD_CONFIG)
|
||||
writeInt(guildCard)
|
||||
writeInt(teamId)
|
||||
// 2092 Bytes of team data.
|
||||
@ -297,7 +348,7 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_
|
||||
}
|
||||
|
||||
// 0x00E5
|
||||
class CharData(buffer: Buffer) : BbMessage(buffer) {
|
||||
class Char(buffer: Buffer) : BbMessage(buffer) {
|
||||
constructor(char: PsoCharacter) : this(
|
||||
buf(0x00E5, 128) {
|
||||
writeInt(char.slot)
|
||||
@ -363,6 +414,26 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_
|
||||
)
|
||||
}
|
||||
|
||||
// 0x00E7
|
||||
class FullCharacterData(buffer: Buffer) : BbMessage(buffer) {
|
||||
constructor(char: PsoCharData) : this(
|
||||
buf(0x00E7, 14744) {
|
||||
repeat(211) { writeInt(0) }
|
||||
repeat(3) { writeShort(0) } // ATP/MST/EVP
|
||||
writeShort(char.hp)
|
||||
repeat(3) { writeShort(0) } // DFP/ATA/LCK
|
||||
writeShort(0) // Unknown.
|
||||
repeat(2) { writeInt(0) } // Unknown.
|
||||
writeInt(char.level)
|
||||
writeInt(char.exp)
|
||||
repeat(2835) { writeInt(0) }
|
||||
writeByteArray(DEFAULT_KEYBOARD_CONFIG)
|
||||
writeByteArray(DEFAULT_GAMEPAD_CONFIG)
|
||||
repeat(527) { writeInt(0) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 0x01E8
|
||||
class Checksum(buffer: Buffer) : BbMessage(buffer) {
|
||||
constructor(checksum: Int) : this(
|
||||
@ -430,21 +501,21 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_
|
||||
code: Int,
|
||||
bodySize: Int = 0,
|
||||
flags: Int = 0,
|
||||
writeBody: WritableCursor.() -> Unit = {},
|
||||
writeBody: (WritableCursor.() -> Unit)? = null,
|
||||
): Buffer {
|
||||
val size = BB_HEADER_SIZE + bodySize
|
||||
val buffer = Buffer.withSize(size)
|
||||
.setShort(BB_MSG_SIZE_POS, size.toShort())
|
||||
.setShort(BB_MSG_CODE_POS, code.toShort())
|
||||
.setInt(BB_MSG_FLAGS_POS, flags)
|
||||
|
||||
val cursor = buffer.cursor()
|
||||
// Write header.
|
||||
.writeShort(size.toShort())
|
||||
.writeShort(code.toShort())
|
||||
.writeInt(flags)
|
||||
if (writeBody != null) {
|
||||
val cursor = buffer.cursor(BB_MSG_BODY_POS)
|
||||
cursor.writeBody()
|
||||
|
||||
cursor.writeBody()
|
||||
|
||||
require(cursor.position == buffer.size) {
|
||||
"Message buffer should be filled completely, only ${cursor.position} / ${buffer.size} bytes written."
|
||||
require(cursor.position == bodySize) {
|
||||
"Message buffer should be filled completely, only ${cursor.position} / $bodySize bytes written."
|
||||
}
|
||||
}
|
||||
|
||||
return buffer
|
||||
@ -533,3 +604,9 @@ enum class MenuType(private val type: Int) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PsoCharData(
|
||||
val hp: Short,
|
||||
val level: Int,
|
||||
val exp: Int,
|
||||
)
|
||||
|
@ -1,6 +1,6 @@
|
||||
package world.phantasmal.psoserv.messages
|
||||
|
||||
val DEFAULT_KEYBOARD_GAMEPAD_CONFIG: ByteArray = ubyteArrayOf(
|
||||
val DEFAULT_KEYBOARD_CONFIG: ByteArray = ubyteArrayOf(
|
||||
0x00u, 0x00u, 0x00u, 0x00u, 0x26u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u,
|
||||
0x00u, 0x00u, 0x22u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u,
|
||||
0x10u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x13u, 0x00u,
|
||||
@ -37,10 +37,14 @@ val DEFAULT_KEYBOARD_GAMEPAD_CONFIG: ByteArray = ubyteArrayOf(
|
||||
0x00u, 0x00u, 0x30u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u,
|
||||
0x31u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x32u, 0x00u,
|
||||
0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x33u, 0x00u, 0x00u, 0x00u,
|
||||
0x01u, 0x00u, 0x00u, 0x00u, 0x00u, 0x01u, 0xffu, 0xffu, 0x00u, 0x00u,
|
||||
0x01u, 0x00u, 0x00u, 0x00u, 0x02u, 0x00u, 0x00u, 0x00u, 0x04u, 0x00u,
|
||||
0x00u, 0x00u, 0x08u, 0x00u, 0x01u, 0x00u, 0x00u, 0x00u, 0x04u, 0x00u,
|
||||
0x00u, 0x00u, 0x02u, 0x00u, 0x00u, 0x00u, 0x08u, 0x00u, 0x00u, 0x00u,
|
||||
0x00u, 0x02u, 0x00u, 0x00u, 0x20u, 0x00u, 0x00u, 0x00u, 0x80u, 0x00u,
|
||||
0x00u, 0x00u, 0x00u, 0x01u, 0x00u, 0x00u, 0x01u, 0x00u, 0x00u, 0x00u,
|
||||
0x01u, 0x00u, 0x00u, 0x00u,
|
||||
).asByteArray()
|
||||
|
||||
val DEFAULT_GAMEPAD_CONFIG: ByteArray = ubyteArrayOf(
|
||||
0x00u, 0x01u, 0xffu, 0xffu, 0x00u, 0x00u, 0x01u, 0x00u, 0x00u, 0x00u,
|
||||
0x02u, 0x00u, 0x00u, 0x00u, 0x04u, 0x00u, 0x00u, 0x00u, 0x08u, 0x00u,
|
||||
0x01u, 0x00u, 0x00u, 0x00u, 0x04u, 0x00u, 0x00u, 0x00u, 0x02u, 0x00u,
|
||||
0x00u, 0x00u, 0x08u, 0x00u, 0x00u, 0x00u, 0x00u, 0x02u, 0x00u, 0x00u,
|
||||
0x20u, 0x00u, 0x00u, 0x00u, 0x80u, 0x00u, 0x00u, 0x00u, 0x00u, 0x01u,
|
||||
0x00u, 0x00u, 0x01u, 0x00u, 0x00u, 0x00u,
|
||||
).asByteArray()
|
||||
|
@ -22,7 +22,7 @@ fun messageString(
|
||||
append("]")
|
||||
}
|
||||
|
||||
data class Header(val code: Int, val size: Int)
|
||||
data class Header(val code: Int, val size: Int, val flags: Int)
|
||||
|
||||
interface Message {
|
||||
val buffer: Buffer
|
||||
@ -30,6 +30,7 @@ interface Message {
|
||||
val size: Int
|
||||
val headerSize: Int
|
||||
val bodySize: Int get() = size - headerSize
|
||||
val flags: Int
|
||||
}
|
||||
|
||||
interface MessageDescriptor<out MessageType : Message> {
|
||||
@ -47,7 +48,7 @@ interface InitEncryptionMessage : Message {
|
||||
|
||||
interface RedirectMessage : Message {
|
||||
var ipAddress: ByteArray
|
||||
var port: Int
|
||||
var port: UShort
|
||||
}
|
||||
|
||||
abstract class AbstractMessage(override val headerSize: Int) : Message {
|
||||
@ -60,8 +61,16 @@ abstract class AbstractMessage(override val headerSize: Int) : Message {
|
||||
protected fun short(offset: Int) = buffer.getShort(headerSize + offset)
|
||||
protected fun int(offset: Int) = buffer.getInt(headerSize + offset)
|
||||
protected fun byteArray(offset: Int, size: Int) = ByteArray(size) { byte(offset + it) }
|
||||
protected fun stringAscii(offset: Int, maxByteLength: Int, nullTerminated: Boolean) =
|
||||
buffer.getStringAscii(headerSize + offset, maxByteLength, nullTerminated)
|
||||
protected fun stringAscii(offset: Int, maxByteLength: Int) =
|
||||
buffer.getStringAscii(headerSize + offset, maxByteLength, nullTerminated = true)
|
||||
|
||||
protected fun setUByte(offset: Int, value: UByte) {
|
||||
buffer.setUByte(headerSize + offset, value)
|
||||
}
|
||||
|
||||
protected fun setUShort(offset: Int, value: UShort) {
|
||||
buffer.setUShort(headerSize + offset, value)
|
||||
}
|
||||
|
||||
protected fun setByte(offset: Int, value: Byte) {
|
||||
buffer.setByte(headerSize + offset, value)
|
||||
@ -77,6 +86,10 @@ abstract class AbstractMessage(override val headerSize: Int) : Message {
|
||||
}
|
||||
}
|
||||
|
||||
protected fun setStringAscii(offset: Int, str: String, byteLength: Int) {
|
||||
buffer.setStringAscii(headerSize + offset, str, byteLength)
|
||||
}
|
||||
|
||||
protected fun messageString(vararg props: Pair<String, Any>): String =
|
||||
messageString(code, size, this::class.simpleName, *props)
|
||||
}
|
||||
|
@ -12,6 +12,8 @@ private const val KEY_SIZE: Int = 4
|
||||
const val PC_HEADER_SIZE: Int = 4
|
||||
const val PC_MSG_SIZE_POS: Int = 0
|
||||
const val PC_MSG_CODE_POS: Int = 2
|
||||
const val PC_MSG_FLAGS_POS: Int = 3
|
||||
const val PC_MSG_BODY_POS: Int = 4
|
||||
|
||||
object PcMessageDescriptor : MessageDescriptor<PcMessage> {
|
||||
override val headerSize: Int = PC_HEADER_SIZE
|
||||
@ -19,8 +21,8 @@ object PcMessageDescriptor : MessageDescriptor<PcMessage> {
|
||||
override fun readHeader(buffer: Buffer): Header {
|
||||
val size = buffer.getUShort(PC_MSG_SIZE_POS).toInt()
|
||||
val code = buffer.getUByte(PC_MSG_CODE_POS).toInt()
|
||||
// Ignore flag byte at position 3.
|
||||
return Header(code, size)
|
||||
val flags = buffer.getUByte(PC_MSG_FLAGS_POS).toInt()
|
||||
return Header(code, size, flags)
|
||||
}
|
||||
|
||||
override fun readMessage(buffer: Buffer): PcMessage =
|
||||
@ -40,6 +42,7 @@ object PcMessageDescriptor : MessageDescriptor<PcMessage> {
|
||||
sealed class PcMessage(override val buffer: Buffer) : AbstractMessage(PC_HEADER_SIZE) {
|
||||
override val code: Int get() = buffer.getUByte(PC_MSG_CODE_POS).toInt()
|
||||
override val size: Int get() = buffer.getUShort(PC_MSG_SIZE_POS).toInt()
|
||||
override val flags: Int get() = buffer.getUByte(PC_MSG_FLAGS_POS).toInt()
|
||||
|
||||
// 0x02
|
||||
class InitEncryption(buffer: Buffer) : PcMessage(buffer), InitEncryptionMessage {
|
||||
@ -76,14 +79,14 @@ sealed class PcMessage(override val buffer: Buffer) : AbstractMessage(PC_HEADER_
|
||||
constructor() : this(buf(0x0D))
|
||||
}
|
||||
|
||||
// 0x10
|
||||
class PatchListOk(buffer: Buffer) : PcMessage(buffer)
|
||||
|
||||
// 0x12
|
||||
class PatchDone(buffer: Buffer) : PcMessage(buffer) {
|
||||
constructor() : this(buf(0x12))
|
||||
}
|
||||
|
||||
// 0x10
|
||||
class PatchListOk(buffer: Buffer) : PcMessage(buffer)
|
||||
|
||||
// 0x13
|
||||
class WelcomeMessage(buffer: Buffer) : PcMessage(buffer) {
|
||||
constructor(message: String) : this(
|
||||
@ -101,24 +104,22 @@ sealed class PcMessage(override val buffer: Buffer) : AbstractMessage(PC_HEADER_
|
||||
require(value.size == 4)
|
||||
setByteArray(0, value)
|
||||
}
|
||||
override var port: Int
|
||||
override var port: UShort
|
||||
get() {
|
||||
buffer.endianness = Endianness.Big
|
||||
val p = uShort(4).toInt()
|
||||
val p = uShort(4)
|
||||
buffer.endianness = Endianness.Little
|
||||
return p
|
||||
}
|
||||
set(value) {
|
||||
require(value in 0..65535)
|
||||
buffer.endianness = Endianness.Big
|
||||
setShort(4, value.toShort())
|
||||
buffer.endianness = Endianness.Little
|
||||
}
|
||||
|
||||
constructor(ipAddress: ByteArray, port: Int) : this(
|
||||
constructor(ipAddress: ByteArray, port: UShort) : this(
|
||||
buf(0x14, 8) {
|
||||
require(ipAddress.size == 4)
|
||||
require(port in 0..65535)
|
||||
|
||||
writeByteArray(ipAddress)
|
||||
endianness = Endianness.Big
|
||||
@ -141,21 +142,22 @@ sealed class PcMessage(override val buffer: Buffer) : AbstractMessage(PC_HEADER_
|
||||
private fun buf(
|
||||
code: Int,
|
||||
bodySize: Int = 0,
|
||||
writeBody: WritableCursor.() -> Unit = {},
|
||||
writeBody: (WritableCursor.() -> Unit)? = null,
|
||||
): Buffer {
|
||||
val size = PC_HEADER_SIZE + bodySize
|
||||
val buffer = Buffer.withSize(size)
|
||||
// Write header.
|
||||
.setShort(PC_MSG_SIZE_POS, size.toShort())
|
||||
.setByte(PC_MSG_CODE_POS, code.toByte())
|
||||
.setByte(PC_MSG_FLAGS_POS, 0) // Flags
|
||||
|
||||
val cursor = buffer.cursor()
|
||||
// Write Header
|
||||
.writeShort(size.toShort())
|
||||
.writeByte(code.toByte())
|
||||
.writeByte(0) // Flags
|
||||
if (writeBody != null) {
|
||||
val cursor = buffer.cursor(PC_MSG_BODY_POS)
|
||||
cursor.writeBody()
|
||||
|
||||
cursor.writeBody()
|
||||
|
||||
require(cursor.position == buffer.size) {
|
||||
"Message buffer should be filled completely, only ${cursor.position} / ${buffer.size} bytes written."
|
||||
require(cursor.position == bodySize) {
|
||||
"Message buffer should be filled completely, only ${cursor.position} / $bodySize bytes written."
|
||||
}
|
||||
}
|
||||
|
||||
return buffer
|
||||
|
@ -19,7 +19,7 @@ class AccountServer(
|
||||
override fun createCipher() = BbCipher()
|
||||
|
||||
override fun createClientReceiver(
|
||||
sender: ClientSender<BbMessage>,
|
||||
ctx: ClientContext<BbMessage>,
|
||||
serverCipher: Cipher,
|
||||
clientCipher: Cipher,
|
||||
): ClientReceiver<BbMessage> = object : ClientReceiver<BbMessage> {
|
||||
@ -31,7 +31,7 @@ class AccountServer(
|
||||
private var charSelected: Boolean = false
|
||||
|
||||
init {
|
||||
sender.send(
|
||||
ctx.send(
|
||||
BbMessage.InitEncryption(
|
||||
"Phantasy Star Online Blue Burst Game Server. Copyright 1999-2004 SONICTEAM.",
|
||||
serverCipher.key,
|
||||
@ -46,7 +46,7 @@ class AccountServer(
|
||||
// TODO: Actual authentication.
|
||||
guildCard = message.guildCard
|
||||
teamId = message.teamId
|
||||
send(
|
||||
ctx.send(
|
||||
BbMessage.AuthData(
|
||||
AuthStatus.Success,
|
||||
guildCard,
|
||||
@ -56,10 +56,10 @@ class AccountServer(
|
||||
)
|
||||
)
|
||||
|
||||
// When the player has selected a character, we send him the list of ships to choose
|
||||
// from.
|
||||
// When the player has selected a character, we send him the list of ships to
|
||||
// choose from.
|
||||
if (message.charSelected) {
|
||||
send(BbMessage.ShipList(ships.map { it.uiName }))
|
||||
ctx.send(BbMessage.ShipList(ships.map { it.uiName }))
|
||||
}
|
||||
|
||||
true
|
||||
@ -67,7 +67,7 @@ class AccountServer(
|
||||
|
||||
is BbMessage.GetAccount -> {
|
||||
// TODO: Send correct guild card number and team ID.
|
||||
send(BbMessage.Account(0, 0))
|
||||
ctx.send(BbMessage.Account(0, 0))
|
||||
|
||||
true
|
||||
}
|
||||
@ -79,7 +79,7 @@ class AccountServer(
|
||||
if (slot in 0..3) {
|
||||
slot = message.slot
|
||||
charSelected = true
|
||||
send(
|
||||
ctx.send(
|
||||
BbMessage.AuthData(
|
||||
AuthStatus.Success,
|
||||
guildCard,
|
||||
@ -88,19 +88,19 @@ class AccountServer(
|
||||
charSelected,
|
||||
)
|
||||
)
|
||||
send(
|
||||
ctx.send(
|
||||
BbMessage.CharSelectAck(slot, CharSelectStatus.Select)
|
||||
)
|
||||
} else {
|
||||
send(
|
||||
ctx.send(
|
||||
BbMessage.CharSelectAck(slot, CharSelectStatus.Nonexistent)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Player is previewing characters.
|
||||
// TODO: Look up character data.
|
||||
send(
|
||||
BbMessage.CharData(
|
||||
ctx.send(
|
||||
BbMessage.Char(
|
||||
PsoCharacter(
|
||||
slot = message.slot,
|
||||
exp = 0,
|
||||
@ -133,13 +133,13 @@ class AccountServer(
|
||||
|
||||
is BbMessage.Checksum -> {
|
||||
// TODO: Checksum checking.
|
||||
send(BbMessage.ChecksumAck(true))
|
||||
ctx.send(BbMessage.ChecksumAck(true))
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
is BbMessage.GetGuildCardHeader -> {
|
||||
send(
|
||||
ctx.send(
|
||||
BbMessage.GuildCardHeader(
|
||||
guildCardBuffer.size,
|
||||
crc32Checksum(guildCardBuffer),
|
||||
@ -158,7 +158,7 @@ class AccountServer(
|
||||
)
|
||||
val size = (guildCardBuffer.size - offset).coerceAtMost(MAX_CHUNK_SIZE)
|
||||
|
||||
send(
|
||||
ctx.send(
|
||||
BbMessage.GuildCardChunk(
|
||||
message.chunkNo,
|
||||
guildCardBuffer.cursor(offset, size),
|
||||
@ -172,7 +172,7 @@ class AccountServer(
|
||||
is BbMessage.GetFileList -> {
|
||||
fileChunkNo = 0
|
||||
|
||||
send(BbMessage.FileList(FILE_LIST))
|
||||
ctx.send(BbMessage.FileList(FILE_LIST))
|
||||
|
||||
true
|
||||
}
|
||||
@ -185,7 +185,7 @@ class AccountServer(
|
||||
MAX_CHUNK_SIZE
|
||||
)
|
||||
|
||||
send(BbMessage.FileChunk(fileChunkNo, FILE_BUFFER.cursor(offset, size)))
|
||||
ctx.send(BbMessage.FileChunk(fileChunkNo, FILE_BUFFER.cursor(offset, size)))
|
||||
|
||||
if (offset + size < FILE_BUFFER.size) {
|
||||
fileChunkNo++
|
||||
@ -197,7 +197,12 @@ class AccountServer(
|
||||
is BbMessage.MenuSelect -> {
|
||||
if (message.menuType == MenuType.Ship) {
|
||||
ships.getOrNull(message.itemId - 1)?.let { ship ->
|
||||
send(BbMessage.Redirect(ship.bindPair.address.address, ship.bindPair.port))
|
||||
ctx.send(
|
||||
BbMessage.Redirect(
|
||||
ship.bindPair.address.address,
|
||||
ship.bindPair.port.toUShort(),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Disconnect.
|
||||
@ -209,11 +214,7 @@ class AccountServer(
|
||||
|
||||
is BbMessage.Disconnect -> false
|
||||
|
||||
else -> unexpectedMessage(message)
|
||||
}
|
||||
|
||||
private fun send(message: BbMessage) {
|
||||
sender.send(message)
|
||||
else -> ctx.unexpectedMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,12 +18,12 @@ class AuthServer(
|
||||
override fun createCipher() = BbCipher()
|
||||
|
||||
override fun createClientReceiver(
|
||||
sender: ClientSender<BbMessage>,
|
||||
ctx: ClientContext<BbMessage>,
|
||||
serverCipher: Cipher,
|
||||
clientCipher: Cipher,
|
||||
): ClientReceiver<BbMessage> = object : ClientReceiver<BbMessage> {
|
||||
init {
|
||||
sender.send(
|
||||
ctx.send(
|
||||
BbMessage.InitEncryption(
|
||||
"Phantasy Star Online Blue Burst Game Server. Copyright 1999-2004 SONICTEAM.",
|
||||
serverCipher.key,
|
||||
@ -36,7 +36,7 @@ class AuthServer(
|
||||
override fun process(message: BbMessage): Boolean = when (message) {
|
||||
is BbMessage.Authenticate -> {
|
||||
// TODO: Actual authentication.
|
||||
send(
|
||||
ctx.send(
|
||||
BbMessage.AuthData(
|
||||
AuthStatus.Success,
|
||||
message.guildCard,
|
||||
@ -45,19 +45,15 @@ class AuthServer(
|
||||
selected = false,
|
||||
)
|
||||
)
|
||||
send(
|
||||
BbMessage.Redirect(accountServerAddress.address, accountServerPort)
|
||||
ctx.send(
|
||||
BbMessage.Redirect(accountServerAddress.address, accountServerPort.toUShort())
|
||||
)
|
||||
|
||||
// Disconnect.
|
||||
false
|
||||
}
|
||||
|
||||
else -> unexpectedMessage(message)
|
||||
}
|
||||
|
||||
private fun send(message: BbMessage) {
|
||||
sender.send(message)
|
||||
else -> ctx.unexpectedMessage(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,11 +5,12 @@ import world.phantasmal.psoserv.encryption.Cipher
|
||||
import world.phantasmal.psoserv.messages.AuthStatus
|
||||
import world.phantasmal.psoserv.messages.BbMessage
|
||||
import world.phantasmal.psoserv.messages.BbMessageDescriptor
|
||||
import world.phantasmal.psoserv.messages.PsoCharData
|
||||
|
||||
class BlockServer(
|
||||
name: String,
|
||||
bindPair: Inet4Pair,
|
||||
private val uiName: String,
|
||||
private val blockNo: Int,
|
||||
) : GameServer<BbMessage>(name, bindPair) {
|
||||
|
||||
override val messageDescriptor = BbMessageDescriptor
|
||||
@ -17,12 +18,12 @@ class BlockServer(
|
||||
override fun createCipher() = BbCipher()
|
||||
|
||||
override fun createClientReceiver(
|
||||
sender: ClientSender<BbMessage>,
|
||||
ctx: ClientContext<BbMessage>,
|
||||
serverCipher: Cipher,
|
||||
clientCipher: Cipher,
|
||||
): ClientReceiver<BbMessage> = object : ClientReceiver<BbMessage> {
|
||||
init {
|
||||
sender.send(
|
||||
ctx.send(
|
||||
BbMessage.InitEncryption(
|
||||
"Phantasy Star Online Blue Burst Game Server. Copyright 1999-2004 SONICTEAM.",
|
||||
serverCipher.key,
|
||||
@ -35,7 +36,7 @@ class BlockServer(
|
||||
override fun process(message: BbMessage): Boolean = when (message) {
|
||||
is BbMessage.Authenticate -> {
|
||||
// TODO: Actual authentication.
|
||||
send(
|
||||
ctx.send(
|
||||
BbMessage.AuthData(
|
||||
AuthStatus.Success,
|
||||
message.guildCard,
|
||||
@ -44,18 +45,37 @@ class BlockServer(
|
||||
message.charSelected,
|
||||
)
|
||||
)
|
||||
send(BbMessage.LobbyList())
|
||||
// TODO: Send 0x00E7
|
||||
send(BbMessage.GetCharacterInfo())
|
||||
ctx.send(BbMessage.LobbyList())
|
||||
ctx.send(
|
||||
BbMessage.FullCharacterData(
|
||||
PsoCharData(
|
||||
hp = 20,
|
||||
level = 0,
|
||||
exp = 0,
|
||||
)
|
||||
)
|
||||
)
|
||||
ctx.send(BbMessage.GetCharData())
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
else -> unexpectedMessage(message)
|
||||
}
|
||||
is BbMessage.CharData -> {
|
||||
ctx.send(
|
||||
BbMessage.JoinLobby(
|
||||
clientId = 0u,
|
||||
leaderId = 0u,
|
||||
disableUdp = true,
|
||||
lobbyNo = 0u,
|
||||
blockNo = blockNo.toUShort(),
|
||||
event = 0u,
|
||||
)
|
||||
)
|
||||
|
||||
private fun send(message: BbMessage) {
|
||||
sender.send(message)
|
||||
true
|
||||
}
|
||||
|
||||
else -> ctx.unexpectedMessage(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,11 @@
|
||||
package world.phantasmal.psoserv.servers
|
||||
|
||||
import mu.KLogger
|
||||
import world.phantasmal.psoserv.encryption.Cipher
|
||||
import world.phantasmal.psoserv.messages.Message
|
||||
import world.phantasmal.psoserv.messages.MessageDescriptor
|
||||
import java.net.Socket
|
||||
|
||||
interface ClientReceiver<MessageType : Message> {
|
||||
fun process(message: MessageType): Boolean
|
||||
}
|
||||
|
||||
interface ClientSender<MessageType : Message> {
|
||||
fun send(message: MessageType, encrypt: Boolean = true)
|
||||
}
|
||||
|
||||
abstract class GameServer<MessageType : Message>(
|
||||
name: String,
|
||||
bindPair: Inet4Pair,
|
||||
@ -33,14 +26,27 @@ abstract class GameServer<MessageType : Message>(
|
||||
protected abstract fun createCipher(): Cipher
|
||||
|
||||
protected abstract fun createClientReceiver(
|
||||
sender: ClientSender<MessageType>,
|
||||
ctx: ClientContext<MessageType>,
|
||||
serverCipher: Cipher,
|
||||
clientCipher: Cipher,
|
||||
): ClientReceiver<MessageType>
|
||||
|
||||
protected fun unexpectedMessage(message: MessageType): Boolean {
|
||||
logger.debug { "Unexpected message: $message." }
|
||||
return true
|
||||
protected interface ClientReceiver<MessageType : Message> {
|
||||
fun process(message: MessageType): Boolean
|
||||
}
|
||||
|
||||
protected class ClientContext<MessageType : Message>(
|
||||
private val logger: KLogger,
|
||||
private val handler: SocketHandler<MessageType>,
|
||||
) {
|
||||
fun send(message: MessageType, encrypt: Boolean = true) {
|
||||
handler.sendMessage(message, encrypt)
|
||||
}
|
||||
|
||||
fun unexpectedMessage(message: MessageType): Boolean {
|
||||
logger.debug { "Unexpected message: $message." }
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private inner class GameClientHandler(client: String, socket: Socket) :
|
||||
@ -49,16 +55,8 @@ abstract class GameServer<MessageType : Message>(
|
||||
private val serverCipher = createCipher()
|
||||
private val clientCipher = createCipher()
|
||||
|
||||
private val handler: ClientReceiver<MessageType> =
|
||||
createClientReceiver(
|
||||
object : ClientSender<MessageType> {
|
||||
override fun send(message: MessageType, encrypt: Boolean) {
|
||||
sendMessage(message, encrypt)
|
||||
}
|
||||
},
|
||||
serverCipher,
|
||||
clientCipher,
|
||||
)
|
||||
private val clientContext = ClientContext(logger, this)
|
||||
private val receiver = createClientReceiver(clientContext, serverCipher, clientCipher)
|
||||
|
||||
override val messageDescriptor = this@GameServer.messageDescriptor
|
||||
|
||||
@ -67,7 +65,7 @@ abstract class GameServer<MessageType : Message>(
|
||||
override val writeEncryptCipher: Cipher = serverCipher
|
||||
|
||||
override fun processMessage(message: MessageType): ProcessResult =
|
||||
if (handler.process(message)) {
|
||||
if (receiver.process(message)) {
|
||||
ProcessResult.Ok
|
||||
} else {
|
||||
// Give the client some time to disconnect.
|
||||
|
@ -15,12 +15,12 @@ class PatchServer(
|
||||
override fun createCipher() = PcCipher()
|
||||
|
||||
override fun createClientReceiver(
|
||||
sender: ClientSender<PcMessage>,
|
||||
ctx: ClientContext<PcMessage>,
|
||||
serverCipher: Cipher,
|
||||
clientCipher: Cipher,
|
||||
): ClientReceiver<PcMessage> = object : ClientReceiver<PcMessage> {
|
||||
init {
|
||||
sender.send(
|
||||
ctx.send(
|
||||
PcMessage.InitEncryption(
|
||||
"Patch Server. Copyright SonicTeam, LTD. 2001",
|
||||
serverCipher.key,
|
||||
@ -32,31 +32,27 @@ class PatchServer(
|
||||
|
||||
override fun process(message: PcMessage): Boolean = when (message) {
|
||||
is PcMessage.InitEncryption -> {
|
||||
send(PcMessage.Login())
|
||||
ctx.send(PcMessage.Login())
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
is PcMessage.Login -> {
|
||||
send(PcMessage.WelcomeMessage(welcomeMessage))
|
||||
send(PcMessage.PatchListStart())
|
||||
send(PcMessage.PatchListEnd())
|
||||
ctx.send(PcMessage.WelcomeMessage(welcomeMessage))
|
||||
ctx.send(PcMessage.PatchListStart())
|
||||
ctx.send(PcMessage.PatchListEnd())
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
is PcMessage.PatchListOk -> {
|
||||
send(PcMessage.PatchDone())
|
||||
ctx.send(PcMessage.PatchDone())
|
||||
|
||||
// Disconnect.
|
||||
false
|
||||
}
|
||||
|
||||
else -> unexpectedMessage(message)
|
||||
}
|
||||
|
||||
private fun send(message: PcMessage) {
|
||||
sender.send(message)
|
||||
else -> ctx.unexpectedMessage(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ class ProxyServer(
|
||||
}
|
||||
|
||||
is RedirectMessage -> {
|
||||
val oldAddress = Inet4Pair(message.ipAddress, message.port)
|
||||
val oldAddress = Inet4Pair(message.ipAddress, message.port.toInt())
|
||||
|
||||
redirectMap[oldAddress]?.let { newAddress ->
|
||||
logger.debug {
|
||||
@ -74,7 +74,7 @@ class ProxyServer(
|
||||
}
|
||||
|
||||
message.ipAddress = newAddress.address.address
|
||||
message.port = newAddress.port
|
||||
message.port = newAddress.port.toUShort()
|
||||
|
||||
return ProcessResult.Changed
|
||||
}
|
||||
|
@ -19,12 +19,12 @@ class ShipServer(
|
||||
override fun createCipher() = BbCipher()
|
||||
|
||||
override fun createClientReceiver(
|
||||
sender: ClientSender<BbMessage>,
|
||||
ctx: ClientContext<BbMessage>,
|
||||
serverCipher: Cipher,
|
||||
clientCipher: Cipher,
|
||||
): ClientReceiver<BbMessage> = object : ClientReceiver<BbMessage> {
|
||||
init {
|
||||
sender.send(
|
||||
ctx.send(
|
||||
BbMessage.InitEncryption(
|
||||
"Phantasy Star Online Blue Burst Game Server. Copyright 1999-2004 SONICTEAM.",
|
||||
serverCipher.key,
|
||||
@ -37,7 +37,7 @@ class ShipServer(
|
||||
override fun process(message: BbMessage): Boolean = when (message) {
|
||||
is BbMessage.Authenticate -> {
|
||||
// TODO: Actual authentication.
|
||||
send(
|
||||
ctx.send(
|
||||
BbMessage.AuthData(
|
||||
AuthStatus.Success,
|
||||
message.guildCard,
|
||||
@ -46,7 +46,7 @@ class ShipServer(
|
||||
message.charSelected,
|
||||
)
|
||||
)
|
||||
send(
|
||||
ctx.send(
|
||||
BbMessage.BlockList(uiName, blocks.map { it.uiName })
|
||||
)
|
||||
|
||||
@ -56,8 +56,11 @@ class ShipServer(
|
||||
is BbMessage.MenuSelect -> {
|
||||
if (message.menuType == MenuType.Block) {
|
||||
blocks.getOrNull(message.itemId - 1)?.let { block ->
|
||||
send(
|
||||
BbMessage.Redirect(block.bindPair.address.address, block.bindPair.port)
|
||||
ctx.send(
|
||||
BbMessage.Redirect(
|
||||
block.bindPair.address.address,
|
||||
block.bindPair.port.toUShort(),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ -68,11 +71,7 @@ class ShipServer(
|
||||
}
|
||||
}
|
||||
|
||||
else -> unexpectedMessage(message)
|
||||
}
|
||||
|
||||
private fun send(message: BbMessage) {
|
||||
sender.send(message)
|
||||
else -> ctx.unexpectedMessage(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user