diff --git a/psolib/src/commonMain/kotlin/world/phantasmal/psolib/buffer/Buffer.kt b/psolib/src/commonMain/kotlin/world/phantasmal/psolib/buffer/Buffer.kt index 8512f62a..57c756b4 100644 --- a/psolib/src/commonMain/kotlin/world/phantasmal/psolib/buffer/Buffer.kt +++ b/psolib/src/commonMain/kotlin/world/phantasmal/psolib/buffer/Buffer.kt @@ -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. */ diff --git a/psolib/src/jsMain/kotlin/world/phantasmal/psolib/buffer/Buffer.kt b/psolib/src/jsMain/kotlin/world/phantasmal/psolib/buffer/Buffer.kt index 5800a244..5fc859bb 100644 --- a/psolib/src/jsMain/kotlin/world/phantasmal/psolib/buffer/Buffer.kt +++ b/psolib/src/jsMain/kotlin/world/phantasmal/psolib/buffer/Buffer.kt @@ -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) diff --git a/psolib/src/jvmMain/kotlin/world/phantasmal/psolib/buffer/Buffer.kt b/psolib/src/jvmMain/kotlin/world/phantasmal/psolib/buffer/Buffer.kt index 575d7619..210594cb 100644 --- a/psolib/src/jvmMain/kotlin/world/phantasmal/psolib/buffer/Buffer.kt +++ b/psolib/src/jvmMain/kotlin/world/phantasmal/psolib/buffer/Buffer.kt @@ -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) diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/Main.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/Main.kt index ade80551..5c0e16b0 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/Main.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/Main.kt @@ -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, ) ) } diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/BbMessages.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/BbMessages.kt index 3db7e9ee..a992b47d 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/BbMessages.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/BbMessages.kt @@ -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 { override val headerSize: Int = BB_HEADER_SIZE @@ -18,8 +20,8 @@ object BbMessageDescriptor : MessageDescriptor { 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 { 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 { 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 { 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, +) diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/DefaultKeyboardGamepadConfig.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/DefaultKeyboardGamepadConfig.kt index 2e950cdc..86d4ecdb 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/DefaultKeyboardGamepadConfig.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/DefaultKeyboardGamepadConfig.kt @@ -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() diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/Messages.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/Messages.kt index 6d9b1a24..8ff7eaea 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/Messages.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/Messages.kt @@ -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 { @@ -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 = messageString(code, size, this::class.simpleName, *props) } diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/PcMessages.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/PcMessages.kt index d4e6c770..2115dcd7 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/PcMessages.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/PcMessages.kt @@ -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 { override val headerSize: Int = PC_HEADER_SIZE @@ -19,8 +21,8 @@ object PcMessageDescriptor : MessageDescriptor { 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 { 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 diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/AccountServer.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/AccountServer.kt index e27c9b2c..65ea15a4 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/AccountServer.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/AccountServer.kt @@ -19,7 +19,7 @@ class AccountServer( override fun createCipher() = BbCipher() override fun createClientReceiver( - sender: ClientSender, + ctx: ClientContext, serverCipher: Cipher, clientCipher: Cipher, ): ClientReceiver = object : ClientReceiver { @@ -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) } } diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/AuthServer.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/AuthServer.kt index 4a86cf0b..f7be0017 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/AuthServer.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/AuthServer.kt @@ -18,12 +18,12 @@ class AuthServer( override fun createCipher() = BbCipher() override fun createClientReceiver( - sender: ClientSender, + ctx: ClientContext, serverCipher: Cipher, clientCipher: Cipher, ): ClientReceiver = object : ClientReceiver { 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) } } } diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/BlockServer.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/BlockServer.kt index b38f352f..18634338 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/BlockServer.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/BlockServer.kt @@ -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(name, bindPair) { override val messageDescriptor = BbMessageDescriptor @@ -17,12 +18,12 @@ class BlockServer( override fun createCipher() = BbCipher() override fun createClientReceiver( - sender: ClientSender, + ctx: ClientContext, serverCipher: Cipher, clientCipher: Cipher, ): ClientReceiver = object : ClientReceiver { 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) } } } diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/GameServer.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/GameServer.kt index ff5662c6..adab6172 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/GameServer.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/GameServer.kt @@ -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 { - fun process(message: MessageType): Boolean -} - -interface ClientSender { - fun send(message: MessageType, encrypt: Boolean = true) -} - abstract class GameServer( name: String, bindPair: Inet4Pair, @@ -33,14 +26,27 @@ abstract class GameServer( protected abstract fun createCipher(): Cipher protected abstract fun createClientReceiver( - sender: ClientSender, + ctx: ClientContext, serverCipher: Cipher, clientCipher: Cipher, ): ClientReceiver - protected fun unexpectedMessage(message: MessageType): Boolean { - logger.debug { "Unexpected message: $message." } - return true + protected interface ClientReceiver { + fun process(message: MessageType): Boolean + } + + protected class ClientContext( + private val logger: KLogger, + private val handler: SocketHandler, + ) { + 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( private val serverCipher = createCipher() private val clientCipher = createCipher() - private val handler: ClientReceiver = - createClientReceiver( - object : ClientSender { - 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( 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. diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/PatchServer.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/PatchServer.kt index f81e2478..e1b0566d 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/PatchServer.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/PatchServer.kt @@ -15,12 +15,12 @@ class PatchServer( override fun createCipher() = PcCipher() override fun createClientReceiver( - sender: ClientSender, + ctx: ClientContext, serverCipher: Cipher, clientCipher: Cipher, ): ClientReceiver = object : ClientReceiver { 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) } } } diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/ProxyServer.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/ProxyServer.kt index aaf0046b..d684bb90 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/ProxyServer.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/ProxyServer.kt @@ -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 } diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/ShipServer.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/ShipServer.kt index 0f7a2e67..887c6d3c 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/ShipServer.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/ShipServer.kt @@ -19,12 +19,12 @@ class ShipServer( override fun createCipher() = BbCipher() override fun createClientReceiver( - sender: ClientSender, + ctx: ClientContext, serverCipher: Cipher, clientCipher: Cipher, ): ClientReceiver = object : ClientReceiver { 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) } } }