diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/Config.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/Config.kt index 76cc7bcc..5b812eb5 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/Config.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/Config.kt @@ -8,21 +8,30 @@ val DEFAULT_CONFIG: Config = Config( auth = ServerConfig(), account = ServerConfig(), proxy = null, - ships = listOf(ShipServerConfig()), + ships = listOf(ShipServerConfig(blocks = listOf("block_1"))), + blocks = listOf(BlockServerConfig(name = "block_1")) ) @Serializable class Config( + /** + * Default address used by any servers when no server-specific address is provided. Itself + * defaults to the loopback address localhost/127.0.0.1. + */ val address: String? = null, val patch: PatchServerConfig? = null, val auth: ServerConfig? = null, val account: ServerConfig? = null, val proxy: ProxyConfig? = null, val ships: List = emptyList(), + val blocks: List = emptyList(), ) @Serializable class ServerConfig( + /** + * Run this server on startup or not. + */ val run: Boolean = true, val address: String? = null, val port: Int? = null, @@ -30,8 +39,39 @@ class ServerConfig( @Serializable class ShipServerConfig( + /** + * Run this server on startup or not. + */ val run: Boolean = true, + /** + * Name for internal use, e.g. logging. + */ val name: String? = null, + /** + * Name shown to players. + */ + val uiName: String? = null, + val address: String? = null, + val port: Int? = null, + /** + * List of internal block names. This ship will redirect to only these blocks. + */ + val blocks: List = emptyList(), +) + +@Serializable +class BlockServerConfig( + /** + * Run this server on startup or not. + */ + val run: Boolean = true, + /** + * Name for internal use, e.g. logging. + */ + val name: String? = null, + /** + * Name shown to players. + */ val uiName: String? = null, val address: String? = null, val port: Int? = null, @@ -39,7 +79,13 @@ class ShipServerConfig( @Serializable class PatchServerConfig( + /** + * Run this server on startup or not. + */ val run: Boolean = true, + /** + * Sent to players when they connect to the patch server. + */ val welcomeMessage: String? = null, val address: String? = null, val port: Int? = null, @@ -47,6 +93,9 @@ class PatchServerConfig( @Serializable class ProxyConfig( + /** + * Run the proxy server on startup or not. + */ val run: Boolean = true, val bindAddress: String? = null, val remoteAddress: String? = null, @@ -55,12 +104,27 @@ class ProxyConfig( @Serializable class ProxyServerConfig( + /** + * Run this proxy server on startup or not. + */ val run: Boolean = true, + /** + * Name for internal use, e.g. logging. + */ val name: String? = null, + /** + * Determines how messages are interpreted and which encryption is used. + */ val version: GameVersionConfig, val bindAddress: String? = null, val bindPort: Int, + /** + * The address of the server that's being proxied. + */ val remoteAddress: String? = null, + /** + * The port of the server that's being proxied. + */ val remotePort: Int, ) diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/Main.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/Main.kt index 812c4a19..a930ed60 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/Main.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/Main.kt @@ -97,20 +97,46 @@ private fun initialize(config: Config): PhantasmalServer { val accountAddress = config.account?.address?.let(::inet4Address) ?: defaultAddress val accountPort = config.account?.port ?: DEFAULT_ACCOUNT_PORT - var shipI = 1 - var shipPort = DEFAULT_FIRST_SHIP_PORT + val shipsToRun = config.ships.filter { it.run } - val ships = config.ships.filter { it.run }.map { shipCfg -> - val ship = ShipInfo( - name = shipCfg.name ?: "ship_$shipI", - uiName = shipCfg.uiName ?: "Ship $shipI", - bindPair = Inet4Pair( - shipCfg.address?.let(::inet4Address) ?: defaultAddress, - shipCfg.port ?: shipPort++, + // Maps block name to block. + val blocks: Map = run { + var blockI = 1 + var blockPort = DEFAULT_FIRST_SHIP_PORT + shipsToRun.size + + config.blocks.filter { it.run }.associate { blockCfg -> + val block = BlockInfo( + name = blockCfg.name ?: "block_$blockI", + uiName = blockCfg.uiName ?: "BLOCK${blockI.toString(2).padStart(2, '0')}", + bindPair = Inet4Pair( + blockCfg.address?.let(::inet4Address) ?: defaultAddress, + blockCfg.port ?: blockPort++, + ), ) - ) - shipI++ - ship + blockI++ + Pair(block.name, block) + } + } + + val ships: List = run { + var shipI = 1 + var shipPort = DEFAULT_FIRST_SHIP_PORT + + shipsToRun.map { shipCfg -> + val ship = ShipInfo( + name = shipCfg.name ?: "ship_$shipI", + uiName = shipCfg.uiName ?: "Ship $shipI", + bindPair = Inet4Pair( + shipCfg.address?.let(::inet4Address) ?: defaultAddress, + shipCfg.port ?: shipPort++, + ), + blocks = shipCfg.blocks.map { + blocks[it] ?: error("""No block with name "$it".""") + }, + ) + shipI++ + ship + } } val servers = mutableListOf() @@ -182,6 +208,21 @@ private fun initialize(config: Config): PhantasmalServer { ship.name, ship.bindPair, ship.uiName, + ship.blocks, + ) + ) + } + + for (block in blocks.values) { + LOGGER.info { + """Configuring block server ${block.name} ("${block.uiName}") to bind to ${block.bindPair}.""" + } + + servers.add( + BlockServer( + block.name, + block.bindPair, + block.uiName, ) ) } diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/Utils.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/Utils.kt index fd501dd8..972d8bc0 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/Utils.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/Utils.kt @@ -1,7 +1,7 @@ package world.phantasmal.psoserv /** - * Rounds [n] up so that it's divisible by [blockSize]. + * Rounds [n] up so that it's divisible by [align]. */ -fun roundToBlockSize(n: Int, blockSize: Int): Int = - n + (blockSize - n % blockSize) % blockSize +fun alignToWidth(n: Int, align: Int): Int = + n + (align - n % align) % align 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 c0283930..3db7e9ee 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/BbMessages.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/BbMessages.kt @@ -57,6 +57,7 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_ override val code: Int get() = buffer.getUShort(BB_MSG_CODE_POS).toInt() override val size: Int get() = buffer.getUShort(BB_MSG_SIZE_POS).toInt() + // 0x0003 class InitEncryption(buffer: Buffer) : BbMessage(buffer), InitEncryptionMessage { override val serverKey: ByteArray get() = byteArray(INIT_MSG_SIZE, size = KEY_SIZE) @@ -76,10 +77,12 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_ ) } + // 0x0005 class Disconnect(buffer: Buffer) : BbMessage(buffer) { constructor() : this(buf(0x0005)) } + // 0x0007 class BlockList(buffer: Buffer) : BbMessage(buffer) { constructor(shipName: String, blocks: List) : this( buf(0x0007, (blocks.size + 1) * 44, flags = blocks.size) { @@ -99,21 +102,23 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_ ) } + // 0x0010 class MenuSelect(buffer: Buffer) : BbMessage(buffer) { val menuType: MenuType get() = MenuType.fromInt(int(0)) - val itemNo: Int get() = int(4) + val itemId: Int get() = int(4) - constructor(menuType: MenuType, itemNo: Int) : this( + constructor(menuType: MenuType, itemId: Int) : this( buf(0x0010, 8) { writeInt(menuType.toInt()) - writeInt(itemNo) + writeInt(itemId) } ) override fun toString(): String = - messageString("menuType" to menuType, "itemNo" to itemNo) + messageString("menuType" to menuType, "itemId" to itemId) } + // 0x0019 class Redirect(buffer: Buffer) : BbMessage(buffer), RedirectMessage { override var ipAddress: ByteArray get() = byteArray(0, size = 4) @@ -146,15 +151,17 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_ ) } + // 0x0083 class LobbyList(buffer: Buffer) : BbMessage(buffer) { constructor() : this( buf(0x0083, 192) { repeat(15) { writeInt(MenuType.Lobby.toInt()) - writeInt(it + 1) // Item no. + writeInt(it + 1) // Item ID. writeInt(0) // Padding. } // 12 zero bytes of padding. + // TODO: Is this necessary? writeInt(0) writeInt(0) writeInt(0) @@ -176,6 +183,12 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_ val charSelected: Boolean get() = byte(137).toInt() != 0 } + // 0x0095 + class GetCharacterInfo(buffer: Buffer) : BbMessage(buffer) { + constructor() : this(buf(0x0095)) + } + + // 0x00A0 class ShipList(buffer: Buffer) : BbMessage(buffer) { constructor(ships: List) : this( buf(0x00A0, (ships.size + 1) * 44, flags = ships.size) { @@ -195,6 +208,7 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_ ) } + // 0x01DC class GuildCardHeader(buffer: Buffer) : BbMessage(buffer) { val guildCardSize: Int get() = int(4) val checksum: Int get() = int(8) @@ -211,6 +225,7 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_ messageString("guildCardSize" to guildCardSize, "checksum" to checksum) } + // 0x02DC class GuildCardChunk(buffer: Buffer) : BbMessage(buffer) { val chunkNo: Int get() = int(4) @@ -238,6 +253,7 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_ // 0x00E0 class GetAccount(buffer: Buffer) : BbMessage(buffer) + // 0x00E2 class Account(buffer: Buffer) : BbMessage(buffer) { constructor(guildCard: Int, teamId: Int) : this( buf(0x00E2, 2804) { @@ -264,6 +280,7 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_ messageString("slot" to slot, "select" to selected) } + // 0x00E4 class CharSelectAck(buffer: Buffer) : BbMessage(buffer) { constructor(slot: Int, status: CharSelectStatus) : this( buf(0x00E4, 8) { @@ -279,6 +296,7 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_ ) } + // 0x00E5 class CharData(buffer: Buffer) : BbMessage(buffer) { constructor(char: PsoCharacter) : this( buf(0x00E5, 128) { @@ -312,6 +330,7 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_ ) } + // 0x00E6 class AuthData(buffer: Buffer) : BbMessage(buffer) { constructor( status: AuthStatus, @@ -344,6 +363,7 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_ ) } + // 0x01E8 class Checksum(buffer: Buffer) : BbMessage(buffer) { constructor(checksum: Int) : this( buf(0x01E8, 8) { @@ -355,6 +375,7 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_ val checksum: Int get() = int(0) } + // 0x02E8 class ChecksumAck(buffer: Buffer) : BbMessage(buffer) { constructor(success: Boolean) : this( buf(0x02E8, 4) { @@ -363,10 +384,12 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_ ) } + // 0x03E8 class GetGuildCardHeader(buffer: Buffer) : BbMessage(buffer) { constructor() : this(buf(0x03E8)) } + // 0x01EB class FileList(buffer: Buffer) : BbMessage(buffer) { constructor(entries: List) : this( buf(0x01EB, entries.size * 76, flags = entries.size) { @@ -380,6 +403,7 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_ ) } + // 0x02EB class FileChunk(buffer: Buffer) : BbMessage(buffer) { constructor(chunkNo: Int, chunk: Cursor) : this( buf(0x02EB, 4 + chunk.size) { @@ -389,10 +413,12 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_ ) } + // 0x03EB class GetFileChunk(buffer: Buffer) : BbMessage(buffer) { constructor() : this(buf(0x03EB)) } + // 0x04EB class GetFileList(buffer: Buffer) : BbMessage(buffer) { constructor() : this(buf(0x04EB)) } 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 eb30d7eb..d4e6c770 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/PcMessages.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/PcMessages.kt @@ -4,7 +4,7 @@ import world.phantasmal.psolib.Endianness import world.phantasmal.psolib.buffer.Buffer import world.phantasmal.psolib.cursor.WritableCursor import world.phantasmal.psolib.cursor.cursor -import world.phantasmal.psoserv.roundToBlockSize +import world.phantasmal.psoserv.alignToWidth private const val INIT_MSG_SIZE: Int = 64 private const val KEY_SIZE: Int = 4 @@ -41,6 +41,7 @@ sealed class PcMessage(override val buffer: Buffer) : AbstractMessage(PC_HEADER_ override val code: Int get() = buffer.getUByte(PC_MSG_CODE_POS).toInt() override val size: Int get() = buffer.getUShort(PC_MSG_SIZE_POS).toInt() + // 0x02 class InitEncryption(buffer: Buffer) : PcMessage(buffer), InitEncryptionMessage { override val serverKey: ByteArray get() = byteArray(INIT_MSG_SIZE, size = KEY_SIZE) @@ -60,32 +61,39 @@ sealed class PcMessage(override val buffer: Buffer) : AbstractMessage(PC_HEADER_ ) } + // 0x04 class Login(buffer: Buffer) : PcMessage(buffer) { constructor() : this(buf(0x04)) } + // 0x0B class PatchListStart(buffer: Buffer) : PcMessage(buffer) { constructor() : this(buf(0x0B)) } + // 0x0D class PatchListEnd(buffer: Buffer) : PcMessage(buffer) { constructor() : this(buf(0x0D)) } + // 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( - buf(0x13, roundToBlockSize(2 * message.length, 4)) { - writeStringUtf16(message, roundToBlockSize(2 * message.length, 4)) + buf(0x13, alignToWidth(2 * message.length, 4)) { + writeStringUtf16(message, alignToWidth(2 * message.length, 4)) } ) } + // 0x14 class Redirect(buffer: Buffer) : PcMessage(buffer), RedirectMessage { override var ipAddress: ByteArray get() = byteArray(0, size = 4) 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 7fe40139..e27c9b2c 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/AccountServer.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/AccountServer.kt @@ -196,7 +196,7 @@ class AccountServer( is BbMessage.MenuSelect -> { if (message.menuType == MenuType.Ship) { - ships.getOrNull(message.itemNo - 1)?.let { ship -> + ships.getOrNull(message.itemId - 1)?.let { ship -> send(BbMessage.Redirect(ship.bindPair.address.address, ship.bindPair.port)) } diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/BlockServer.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/BlockServer.kt new file mode 100644 index 00000000..b38f352f --- /dev/null +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/BlockServer.kt @@ -0,0 +1,61 @@ +package world.phantasmal.psoserv.servers + +import world.phantasmal.psoserv.encryption.BbCipher +import world.phantasmal.psoserv.encryption.Cipher +import world.phantasmal.psoserv.messages.AuthStatus +import world.phantasmal.psoserv.messages.BbMessage +import world.phantasmal.psoserv.messages.BbMessageDescriptor + +class BlockServer( + name: String, + bindPair: Inet4Pair, + private val uiName: String, +) : GameServer(name, bindPair) { + + override val messageDescriptor = BbMessageDescriptor + + override fun createCipher() = BbCipher() + + override fun createClientReceiver( + sender: ClientSender, + serverCipher: Cipher, + clientCipher: Cipher, + ): ClientReceiver = object : ClientReceiver { + init { + sender.send( + BbMessage.InitEncryption( + "Phantasy Star Online Blue Burst Game Server. Copyright 1999-2004 SONICTEAM.", + serverCipher.key, + clientCipher.key, + ), + encrypt = false, + ) + } + + override fun process(message: BbMessage): Boolean = when (message) { + is BbMessage.Authenticate -> { + // TODO: Actual authentication. + send( + BbMessage.AuthData( + AuthStatus.Success, + message.guildCard, + message.teamId, + message.charSlot, + message.charSelected, + ) + ) + send(BbMessage.LobbyList()) + // TODO: Send 0x00E7 + send(BbMessage.GetCharacterInfo()) + + true + } + + else -> unexpectedMessage(message) + } + + private fun send(message: BbMessage) { + sender.send(message) + } + } +} diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/ShipInfo.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/ShipInfo.kt index 996b468c..61415121 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/ShipInfo.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/ShipInfo.kt @@ -4,4 +4,11 @@ class ShipInfo( val name: String, val uiName: String, val bindPair: Inet4Pair, + val blocks: List, +) + +class BlockInfo( + val name: String, + val uiName: String, + val bindPair: Inet4Pair, ) 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 80470936..0f7a2e67 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/ShipServer.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/ShipServer.kt @@ -5,11 +5,13 @@ 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.MenuType class ShipServer( name: String, bindPair: Inet4Pair, private val uiName: String, + private val blocks: List, ) : GameServer(name, bindPair) { override val messageDescriptor = BbMessageDescriptor @@ -45,12 +47,27 @@ class ShipServer( ) ) send( - BbMessage.BlockList(uiName, listOf("BLOCK01")) + BbMessage.BlockList(uiName, blocks.map { it.uiName }) ) true } + 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) + ) + } + + // Disconnect. + false + } else { + true + } + } + else -> unexpectedMessage(message) } diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/SocketHandler.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/SocketHandler.kt index 22b9b4d8..d64fa7ec 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/SocketHandler.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/SocketHandler.kt @@ -7,7 +7,7 @@ import world.phantasmal.psoserv.encryption.Cipher import world.phantasmal.psoserv.messages.Message import world.phantasmal.psoserv.messages.MessageDescriptor import world.phantasmal.psoserv.messages.messageString -import world.phantasmal.psoserv.roundToBlockSize +import world.phantasmal.psoserv.alignToWidth import java.net.Socket import java.net.SocketException import kotlin.math.min @@ -69,7 +69,7 @@ abstract class SocketHandler( } val (code, size) = messageDescriptor.readHeader(headerBuffer) - val encryptedSize = roundToBlockSize(size, decryptCipher?.blockSize ?: 1) + val encryptedSize = alignToWidth(size, decryptCipher?.blockSize ?: 1) // Bytes available for the next message. val available = readBuffer.size - offset @@ -242,7 +242,7 @@ abstract class SocketHandler( // Pad buffer before encrypting. val initialSize = message.buffer.size buffer = message.buffer.copy( - size = roundToBlockSize(initialSize, cipher.blockSize) + size = alignToWidth(initialSize, cipher.blockSize) ) cipher.encrypt(buffer) } else {