diff --git a/buildSrc/gradle.properties b/buildSrc/gradle.properties new file mode 100644 index 00000000..5b52f144 --- /dev/null +++ b/buildSrc/gradle.properties @@ -0,0 +1,2 @@ +kotlin.code.style=official +kotlin.mpp.stability.nowarn=true diff --git a/buildSrc/src/main/kotlin/world/phantasmal/OptIns.kt b/buildSrc/src/main/kotlin/world/phantasmal/ExperimentalAnnotations.kt similarity index 100% rename from buildSrc/src/main/kotlin/world/phantasmal/OptIns.kt rename to buildSrc/src/main/kotlin/world/phantasmal/ExperimentalAnnotations.kt diff --git a/buildSrc/src/main/kotlin/world/phantasmal/jvm.gradle.kts b/buildSrc/src/main/kotlin/world/phantasmal/jvm.gradle.kts index 25ac80ef..61047fbc 100644 --- a/buildSrc/src/main/kotlin/world/phantasmal/jvm.gradle.kts +++ b/buildSrc/src/main/kotlin/world/phantasmal/jvm.gradle.kts @@ -13,7 +13,9 @@ val log4jVersion: String by project.extra tasks.withType().configureEach { kotlinOptions { jvmTarget = "11" - freeCompilerArgs = freeCompilerArgs + EXPERIMENTAL_ANNOTATION_COMPILER_ARGS + freeCompilerArgs = freeCompilerArgs + + EXPERIMENTAL_ANNOTATION_COMPILER_ARGS + + "-Xjvm-default=all" } } diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/Main.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/Main.kt index 21f5c769..c8a6960d 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/Main.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/Main.kt @@ -5,6 +5,7 @@ import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.hocon.Hocon import kotlinx.serialization.hocon.decodeFromConfig import mu.KotlinLogging +import world.phantasmal.psoserv.data.AccountStore import world.phantasmal.psoserv.encryption.BbCipher import world.phantasmal.psoserv.encryption.Cipher import world.phantasmal.psoserv.encryption.PcCipher @@ -71,7 +72,8 @@ fun main(args: Array) { } // Initialize and start the server. - val servers = initialize(config) + val accountStore = AccountStore(LOGGER) + val servers = initialize(config, accountStore) if (servers.isEmpty()) { LOGGER.info { "No servers configured, stopping." } @@ -85,7 +87,7 @@ fun main(args: Array) { } } -private fun initialize(config: Config): List { +private fun initialize(config: Config, accountStore: AccountStore): List { val address = config.address?.let(::inet4Address) ?: DEFAULT_ADDRESS LOGGER.info { "Binding to $address." } @@ -96,16 +98,22 @@ private fun initialize(config: Config): List { val blocks: Map = run { var blockI = 1 var blockPort = DEFAULT_FIRST_SHIP_PORT + config.ships.size + val blocks = mutableMapOf() - config.blocks.associate { blockCfg -> + for (blockCfg in config.blocks) { val block = BlockInfo( name = validateName("Block", blockCfg.name) ?: "block_$blockI", uiName = blockCfg.uiName ?: "BLOCK${blockI.toString(2).padStart(2, '0')}", bindPair = Inet4Pair(address, blockCfg.port ?: blockPort++), ) blockI++ - Pair(block.name, block) + + require(blocks.put(block.name, block) == null) { + """Duplicate block with name ${block.name}.""" + } } + + blocks } val ships: List = run { @@ -118,7 +126,7 @@ private fun initialize(config: Config): List { uiName = shipCfg.uiName ?: "Ship $shipI", bindPair = Inet4Pair(address, shipCfg.port ?: shipPort++), blocks = shipCfg.blocks.map { - blocks[it] ?: error("""No block with name "$it".""") + blocks[it] ?: error("""No block with name $it.""") }, ) shipI++ @@ -164,12 +172,13 @@ private fun initialize(config: Config): List { LOGGER.info { "Configuring account server to bind to port ${bindPair.port}." } LOGGER.info { "Account server will redirect to ${ships.size} ship servers: ${ - ships.joinToString { """"${it.name}" (port ${it.bindPair.port})""" } + ships.joinToString { """${it.name} (port ${it.bindPair.port})""" } }." } servers.add( AccountServer( + accountStore, bindPair, ships, ) @@ -198,9 +207,10 @@ private fun initialize(config: Config): List { servers.add( BlockServer( + accountStore, block.name, block.bindPair, - blockNo = index + 1, + blockId = index + 1, ) ) } diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/data/Account.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/data/Account.kt new file mode 100644 index 00000000..5c8e3985 --- /dev/null +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/data/Account.kt @@ -0,0 +1,47 @@ +package world.phantasmal.psoserv.data + +class Account( + val id: Long, + val username: String, + val guildCardNo: Int, + val teamId: Int, + val characters: List, +) { + init { + require(username.length <= 16) + } +} + +class PlayingAccount( + val account: Account, + val char: Character, + val blockId: Int, +) + +class Character( + val id: Long, + val accountId: Long, + val name: String, + val sectionId: SectionId, + val exp: Int, + val level: Int, +) { + init { + require(name.length <= 16) + require(exp >= 0) + require(level in 1..200) + } +} + +enum class SectionId { + Viridia, + Greenill, + Skyly, + Bluefull, + Purplenum, + Pinkal, + Redria, + Oran, + Yellowboze, + Whitill, +} diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/data/AccountStore.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/data/AccountStore.kt new file mode 100644 index 00000000..6a7159f3 --- /dev/null +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/data/AccountStore.kt @@ -0,0 +1,111 @@ +package world.phantasmal.psoserv.data + +import mu.KLogger + +class AccountStore(private val logger: KLogger) { + private var nextId: Long = 1L + private var nextGuildCardNo: Int = 1 + + /** + * Maps usernames to accounts. Accounts are created on the fly. + */ + private val accounts = mutableMapOf() + + /** + * Logged in accounts must always be logged out with [logOut]. + */ + fun logIn(username: String, password: String): LogInResult = + synchronized(this) { + val data = accounts.getOrPut(username) { + val accountId = nextId++ + AccountData( + account = Account( + id = accountId, + username = username, + guildCardNo = nextGuildCardNo++, + teamId = 1337, + characters = listOf( + Character( + id = nextId++, + accountId = accountId, + name = "${username.take(14)} 1", + sectionId = SectionId.Viridia, + exp = 1_000_000, + level = 200, + ) + ), + ), + playing = null, + password = password, + loggedIn = false, + ) + } + + if (password != data.password) { + LogInResult.BadPassword + } else if (data.loggedIn) { + LogInResult.AlreadyLoggedIn + } else { + data.loggedIn = true + LogInResult.Ok(data.account) + } + } + + fun logOut(accountId: Long) { + synchronized(this) { + val data = accounts.values.find { it.account.id == accountId } + + if (data == null) { + logger.warn { + "Trying to log out nonexistent account $accountId." + } + } else { + if (!data.loggedIn) { + logger.warn { + """Trying to log out account ${data.account.id} "${data.account.username}" while it wasn't logged in.""" + } + } + + data.playing = null + data.loggedIn = false + } + } + } + + fun getAccountById(accountId: Long): Account? = + synchronized(this) { + accounts.values.find { it.account.id == accountId }?.account + } + + fun setAccountPlaying(accountId: Long, char: Character, blockId: Int): Account { + synchronized(this) { + val data = accounts.values.first { it.account.id == accountId } + data.playing = PlayingAccount(data.account, char, blockId) + return data.account + } + } + + fun getAccountsByBlock(blockId: Int): List = + synchronized(this) { + accounts.values + .filter { it.loggedIn && it.playing?.blockId == blockId } + .mapNotNull { it.playing } + } + + sealed class LogInResult { + class Ok(val account: Account) : LogInResult() + object BadPassword : LogInResult() + object AlreadyLoggedIn : LogInResult() + } + + private class AccountData( + var account: Account, + var playing: PlayingAccount?, + val password: String, + var loggedIn: Boolean, + ) { + init { + require(password.length <= 16) + } + } +} 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 a992b47d..e24e2653 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/BbMessages.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/BbMessages.kt @@ -180,8 +180,9 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_ lobbyNo: UByte, blockNo: UShort, event: UShort, + players: List, ) : this( - buf(0x0067, 12 + 1312, flags = 1) { // TODO: Set flags to player count. + buf(0x0067, 12 + players.size * 1312, flags = players.size) { writeUByte(clientId) writeUByte(leaderId) writeByte(if (disableUdp) 1 else 0) @@ -189,7 +190,16 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_ writeUShort(blockNo) writeUShort(event) writeInt(0) // Unused. - repeat(328) { writeInt(0) } + + for (player in players) { + writeInt(player.playerTag) + writeInt(player.guildCardNo) + repeat(5) { writeInt(0) } // Unknown. + writeInt(player.clientId) + writeStringUtf16(player.charName, 32) + writeInt(0) // Unknown. + repeat(311) { writeInt(0) } + } } ) @@ -223,10 +233,10 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_ // 0x0093 class Authenticate(buffer: Buffer) : BbMessage(buffer) { - val guildCard: Int get() = int(4) + val guildCardNo: 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) + 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() @@ -301,17 +311,19 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_ } // 0x00E0 - class GetAccount(buffer: Buffer) : BbMessage(buffer) + class GetAccount(buffer: Buffer) : BbMessage(buffer) { + constructor() : this(buf(0x00E1)) + } // 0x00E2 class Account(buffer: Buffer) : BbMessage(buffer) { - constructor(guildCard: Int, teamId: Int) : this( + constructor(guildCardNo: Int, teamId: Int) : this( buf(0x00E2, 2804) { // 276 Bytes of unknown data. repeat(69) { writeInt(0) } writeByteArray(DEFAULT_KEYBOARD_CONFIG) writeByteArray(DEFAULT_GAMEPAD_CONFIG) - writeInt(guildCard) + writeInt(guildCardNo) writeInt(teamId) // 2092 Bytes of team data. repeat(523) { writeInt(0) } @@ -383,12 +395,26 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_ // 0x00E6 class AuthData(buffer: Buffer) : BbMessage(buffer) { + var status: AuthStatus + get() = when (val value = int(0)) { + 0 -> AuthStatus.Success + 1 -> AuthStatus.Error + 8 -> AuthStatus.Nonexistent + else -> AuthStatus.Unknown(value) + } + set(status) = setInt(0, when (status) { + AuthStatus.Success -> 0 + AuthStatus.Error -> 1 + AuthStatus.Nonexistent -> 8 + is AuthStatus.Unknown -> status.value + }) + constructor( status: AuthStatus, - guildCard: Int, + guildCardNo: Int, teamId: Int, - slot: Int, - selected: Boolean, + charSlot: Int, + charSelected: Boolean, ) : this( buf(0x00E6, 60) { writeInt( @@ -396,27 +422,31 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_ AuthStatus.Success -> 0 AuthStatus.Error -> 1 AuthStatus.Nonexistent -> 8 + is AuthStatus.Unknown -> status.value } ) writeInt(0x10000) - writeInt(guildCard) + writeInt(guildCardNo) writeInt(teamId) writeInt( if (status == AuthStatus.Success) (0xDEADBEEF).toInt() else 0 ) - writeByte(slot.toByte()) - writeByte(if (selected) 1 else 0) + writeByte(charSlot.toByte()) + writeByte(if (charSelected) 1 else 0) // 34 Bytes of unknown data. writeShort(0) repeat(8) { writeInt(0) } writeInt(0x102) } ) + + override fun toString(): String = + messageString("status" to status) } // 0x00E7 class FullCharacterData(buffer: Buffer) : BbMessage(buffer) { - constructor(char: PsoCharData) : this( + constructor(char: PsoCharData, name: String, sectionId: Byte, charClass: Byte) : this( buf(0x00E7, 14744) { repeat(211) { writeInt(0) } repeat(3) { writeShort(0) } // ATP/MST/EVP @@ -426,7 +456,15 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_ repeat(2) { writeInt(0) } // Unknown. writeInt(char.level) writeInt(char.exp) - repeat(2835) { writeInt(0) } + repeat(92) { writeInt(0) } // Rest of char. + repeat(1275) { writeInt(0) } + writeStringUtf16(name, byteLength = 48) + repeat(8) { writeInt(0) } // Team name. + repeat(44) { writeInt(0) } // Guild card description. + writeShort(0) // Reserved. + writeByte(sectionId) + writeByte(charClass) + repeat(1403) { writeInt(0) } writeByteArray(DEFAULT_KEYBOARD_CONFIG) writeByteArray(DEFAULT_GAMEPAD_CONFIG) repeat(527) { writeInt(0) } @@ -523,8 +561,15 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_ } } -enum class AuthStatus { - Success, Error, Nonexistent +sealed class AuthStatus { + object Success : AuthStatus() + object Error : AuthStatus() + object Nonexistent : AuthStatus() + class Unknown(val value: Int) : AuthStatus() { + override fun toString(): String = "Unknown[$value]" + } + + override fun toString(): String = this::class.simpleName!! } class PsoCharacter( @@ -561,7 +606,7 @@ class GuildCardEntry( ) class GuildCard( - val entries: List + val entries: List, ) class FileListEntry( @@ -610,3 +655,10 @@ class PsoCharData( val level: Int, val exp: Int, ) + +class LobbyPlayer( + val playerTag: Int, + val guildCardNo: Int, + val clientId: Int, + val charName: String, +) 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 8ff7eaea..3389f50f 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/Messages.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/Messages.kt @@ -80,6 +80,10 @@ abstract class AbstractMessage(override val headerSize: Int) : Message { buffer.setShort(headerSize + offset, value) } + protected fun setInt(offset: Int, value: Int) { + buffer.setInt(headerSize + offset, value) + } + protected fun setByteArray(offset: Int, array: ByteArray) { for ((index, byte) in array.withIndex()) { setByte(offset + index, byte) 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 65ea15a4..f2926730 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/AccountServer.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/AccountServer.kt @@ -4,12 +4,15 @@ import world.phantasmal.core.math.clamp import world.phantasmal.psolib.Endianness import world.phantasmal.psolib.buffer.Buffer import world.phantasmal.psolib.cursor.cursor +import world.phantasmal.psoserv.data.AccountStore +import world.phantasmal.psoserv.data.AccountStore.LogInResult import world.phantasmal.psoserv.encryption.BbCipher import world.phantasmal.psoserv.encryption.Cipher import world.phantasmal.psoserv.messages.* import world.phantasmal.psoserv.utils.crc32Checksum class AccountServer( + private val accountStore: AccountStore, bindPair: Inet4Pair, private val ships: List, ) : GameServer("account", bindPair) { @@ -23,11 +26,10 @@ class AccountServer( serverCipher: Cipher, clientCipher: Cipher, ): ClientReceiver = object : ClientReceiver { + private var accountId: Long? = null private val guildCardBuffer = Buffer.withSize(54672) private var fileChunkNo = 0 - private var guildCard: Int = -1 - private var teamId: Int = -1 - private var slot: Int = 0 + private var charSlot: Int = 0 private var charSelected: Boolean = false init { @@ -43,88 +45,124 @@ class AccountServer( override fun process(message: BbMessage): Boolean = when (message) { is BbMessage.Authenticate -> { - // TODO: Actual authentication. - guildCard = message.guildCard - teamId = message.teamId - ctx.send( - BbMessage.AuthData( - AuthStatus.Success, - guildCard, - teamId, - slot, - charSelected, + when ( + val result = accountStore.logIn( + message.username, + message.password, ) - ) + ) { + is LogInResult.Ok -> { + val account = result.account + this.accountId = account.id - // When the player has selected a character, we send him the list of ships to - // choose from. - if (message.charSelected) { - ctx.send(BbMessage.ShipList(ships.map { it.uiName })) + charSlot = message.charSlot + charSelected = message.charSelected + + ctx.send( + BbMessage.AuthData( + AuthStatus.Success, + account.guildCardNo, + account.teamId, + charSlot, + charSelected, + ) + ) + + // When the player has selected a character, we send him the list of ships + // to choose from. + if (charSelected) { + ctx.send(BbMessage.ShipList(ships.map { it.uiName })) + } + } + LogInResult.BadPassword -> { + ctx.send( + BbMessage.AuthData( + AuthStatus.Nonexistent, + message.guildCardNo, + message.teamId, + message.charSlot, + message.charSelected, + ) + ) + } + LogInResult.AlreadyLoggedIn -> { + ctx.send( + BbMessage.AuthData( + AuthStatus.Error, + message.guildCardNo, + message.teamId, + message.charSlot, + message.charSelected, + ) + ) + } } true } is BbMessage.GetAccount -> { - // TODO: Send correct guild card number and team ID. - ctx.send(BbMessage.Account(0, 0)) + accountId?.let(accountStore::getAccountById)?.let { + ctx.send(BbMessage.Account(it.guildCardNo, it.teamId)) + } true } is BbMessage.CharSelect -> { - if (message.selected) { - // Player has chosen a character. - // TODO: Verify slot. - if (slot in 0..3) { - slot = message.slot + val account = accountId?.let(accountStore::getAccountById) + + if (account != null && message.slot in account.characters.indices) { + if (message.selected) { + // Player has chosen a character. + charSlot = message.slot charSelected = true ctx.send( BbMessage.AuthData( AuthStatus.Success, - guildCard, - teamId, - slot, + account.guildCardNo, + account.teamId, + charSlot, charSelected, ) ) ctx.send( - BbMessage.CharSelectAck(slot, CharSelectStatus.Select) + BbMessage.CharSelectAck(charSlot, CharSelectStatus.Select) ) } else { + // Player is previewing characters. + val char = account.characters[message.slot] ctx.send( - BbMessage.CharSelectAck(slot, CharSelectStatus.Nonexistent) + BbMessage.Char( + PsoCharacter( + slot = message.slot, + exp = char.exp, + level = char.level - 1, + guildCardString = "", + nameColor = 0, + model = 0, + nameColorChecksum = 0, + sectionId = char.sectionId.ordinal, + characterClass = 0, + costume = 0, + skin = 0, + face = 0, + head = 0, + hair = 0, + hairRed = 0, + hairGreen = 0, + hairBlue = 0, + propX = 0.5, + propY = 0.5, + name = char.name, + playTime = 0, + ) + ) ) } } else { - // Player is previewing characters. - // TODO: Look up character data. ctx.send( - BbMessage.Char( - PsoCharacter( - slot = message.slot, - exp = 0, - level = 0, - guildCardString = "", - nameColor = 0, - model = 0, - nameColorChecksum = 0, - sectionId = message.slot, - characterClass = message.slot, - costume = 0, - skin = 0, - face = 0, - head = 0, - hair = 0, - hairRed = 0, - hairGreen = 0, - hairBlue = 0, - propX = 0.5, - propY = 0.5, - name = "Phantasmal ${message.slot}", - playTime = 0, - ) - ) + BbMessage.CharSelectAck(message.slot, CharSelectStatus.Nonexistent) ) } @@ -205,17 +243,34 @@ class AccountServer( ) } - // Disconnect. + // Log out and disconnect. + logOut() false } else { true } } - is BbMessage.Disconnect -> false + is BbMessage.Disconnect -> { + // Log out and disconnect. + logOut() + false + } else -> ctx.unexpectedMessage(message) } + + override fun connectionClosed() { + logOut() + } + + private fun logOut() { + try { + accountId?.let(accountStore::logOut) + } finally { + accountId = null + } + } } companion object { 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 f7be0017..693aaf3f 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/AuthServer.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/AuthServer.kt @@ -35,14 +35,15 @@ class AuthServer( override fun process(message: BbMessage): Boolean = when (message) { is BbMessage.Authenticate -> { - // TODO: Actual authentication. + // Don't actually authenticate, since we're simply redirecting the player to the + // account server. ctx.send( BbMessage.AuthData( AuthStatus.Success, - message.guildCard, + message.guildCardNo, message.teamId, - slot = 0, - selected = false, + charSlot = 0, + charSelected = false, ) ) ctx.send( 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 18634338..38841199 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/BlockServer.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/BlockServer.kt @@ -1,16 +1,16 @@ package world.phantasmal.psoserv.servers +import world.phantasmal.psoserv.data.AccountStore +import world.phantasmal.psoserv.data.AccountStore.LogInResult 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 -import world.phantasmal.psoserv.messages.PsoCharData +import world.phantasmal.psoserv.messages.* class BlockServer( + private val accountStore: AccountStore, name: String, bindPair: Inet4Pair, - private val blockNo: Int, + private val blockId: Int, ) : GameServer(name, bindPair) { override val messageDescriptor = BbMessageDescriptor @@ -22,6 +22,8 @@ class BlockServer( serverCipher: Cipher, clientCipher: Cipher, ): ClientReceiver = object : ClientReceiver { + private var accountId: Long? = null + init { ctx.send( BbMessage.InitEncryption( @@ -35,27 +37,78 @@ class BlockServer( override fun process(message: BbMessage): Boolean = when (message) { is BbMessage.Authenticate -> { - // TODO: Actual authentication. - ctx.send( - BbMessage.AuthData( - AuthStatus.Success, - message.guildCard, - message.teamId, - message.charSlot, - message.charSelected, - ) - ) - ctx.send(BbMessage.LobbyList()) - ctx.send( - BbMessage.FullCharacterData( - PsoCharData( - hp = 20, - level = 0, - exp = 0, + when ( + val result = accountStore.logIn(message.username, message.password) + ) { + is LogInResult.Ok -> { + accountId = result.account.id + val char = result.account.characters.getOrNull(message.charSlot) + + if (char == null) { + ctx.send( + BbMessage.AuthData( + AuthStatus.Nonexistent, + message.guildCardNo, + message.teamId, + message.charSlot, + message.charSelected, + ) + ) + } else { + val account = accountStore.setAccountPlaying( + result.account.id, + char, + blockId, + ) + ctx.send( + BbMessage.AuthData( + AuthStatus.Success, + account.guildCardNo, + account.teamId, + message.charSlot, + message.charSelected, + ) + ) + + ctx.send(BbMessage.LobbyList()) + ctx.send( + BbMessage.FullCharacterData( + PsoCharData( + hp = 0, + level = char.level - 1, + exp = char.exp, + ), + char.name, + char.sectionId.ordinal.toByte(), + charClass = 0, + ) + ) + ctx.send(BbMessage.GetCharData()) + } + } + LogInResult.BadPassword -> { + ctx.send( + BbMessage.AuthData( + AuthStatus.Nonexistent, + message.guildCardNo, + message.teamId, + message.charSlot, + message.charSelected, + ) ) - ) - ) - ctx.send(BbMessage.GetCharData()) + } + LogInResult.AlreadyLoggedIn -> { + ctx.send( + BbMessage.AuthData( + AuthStatus.Error, + message.guildCardNo, + message.teamId, + message.charSlot, + message.charSelected, + ) + ) + } + } true } @@ -67,15 +120,41 @@ class BlockServer( leaderId = 0u, disableUdp = true, lobbyNo = 0u, - blockNo = blockNo.toUShort(), + blockNo = blockId.toUShort(), event = 0u, + players = accountStore.getAccountsByBlock(blockId).map { + LobbyPlayer( + playerTag = 0, + guildCardNo = it.account.guildCardNo, + clientId = 0, + charName = it.char.name, + ) + } ) ) true } + is BbMessage.Disconnect -> { + // Log out and disconnect. + logOut() + false + } + else -> ctx.unexpectedMessage(message) } + + override fun connectionClosed() { + logOut() + } + + private fun logOut() { + try { + accountId?.let(accountStore::logOut) + } finally { + accountId = null + } + } } } 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 adab6172..2a275d9f 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/GameServer.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/GameServer.kt @@ -33,6 +33,7 @@ abstract class GameServer( protected interface ClientReceiver { fun process(message: MessageType): Boolean + fun connectionClosed() {} } protected class ClientContext( @@ -74,5 +75,9 @@ abstract class GameServer( // Close the connection. ProcessResult.Done } + + override fun socketClosed() { + receiver.connectionClosed() + } } } 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 887c6d3c..b110b6b8 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/ShipServer.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/ShipServer.kt @@ -36,11 +36,12 @@ class ShipServer( override fun process(message: BbMessage): Boolean = when (message) { is BbMessage.Authenticate -> { - // TODO: Actual authentication. + // Don't actually authenticate, since we're simply letting the player choose a block + // and then redirecting him to the corresponding block server. ctx.send( BbMessage.AuthData( AuthStatus.Success, - message.guildCard, + message.guildCardNo, message.teamId, message.charSlot, message.charSelected, 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 47e761d9..d479d40e 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/SocketHandler.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/SocketHandler.kt @@ -48,6 +48,7 @@ abstract class SocketHandler( if (readSize == -1) { // Close the connection if no more bytes available. + logger.debug { "$name ($sockName) end of stream." } break@readLoop }