Added block servers.

This commit is contained in:
Daan Vanden Bosch 2021-08-03 20:56:56 +02:00
parent c627b33a51
commit f43e154bc7
10 changed files with 253 additions and 29 deletions

View File

@ -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<ShipServerConfig> = emptyList(),
val blocks: List<BlockServerConfig> = 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<String> = 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,
)

View File

@ -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<String, BlockInfo> = 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<ShipInfo> = 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<Server>()
@ -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,
)
)
}

View File

@ -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

View File

@ -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<String>) : 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<String>) : 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<FileListEntry>) : 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))
}

View File

@ -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)

View File

@ -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))
}

View File

@ -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<BbMessage>(name, bindPair) {
override val messageDescriptor = BbMessageDescriptor
override fun createCipher() = BbCipher()
override fun createClientReceiver(
sender: ClientSender<BbMessage>,
serverCipher: Cipher,
clientCipher: Cipher,
): ClientReceiver<BbMessage> = object : ClientReceiver<BbMessage> {
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)
}
}
}

View File

@ -4,4 +4,11 @@ class ShipInfo(
val name: String,
val uiName: String,
val bindPair: Inet4Pair,
val blocks: List<BlockInfo>,
)
class BlockInfo(
val name: String,
val uiName: String,
val bindPair: Inet4Pair,
)

View File

@ -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<BlockInfo>,
) : GameServer<BbMessage>(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)
}

View File

@ -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<MessageType : Message>(
}
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<MessageType : Message>(
// 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 {