Added lobbies and party creation. Broadcast messages are now broadcast to other clients (without verification).

This commit is contained in:
Daan Vanden Bosch 2021-08-25 21:19:21 +02:00
parent b038851a29
commit c2f50e6827
13 changed files with 957 additions and 322 deletions

View File

@ -118,8 +118,8 @@ interface Cursor {
*/
fun stringAscii(
maxByteLength: Int,
nullTerminated: Boolean,
dropRemaining: Boolean,
nullTerminated: Boolean = true,
dropRemaining: Boolean = true,
): String
/**
@ -127,8 +127,8 @@ interface Cursor {
*/
fun stringUtf16(
maxByteLength: Int,
nullTerminated: Boolean,
dropRemaining: Boolean,
nullTerminated: Boolean = true,
dropRemaining: Boolean = true,
): String
/**

View File

@ -100,7 +100,7 @@ actual class Buffer private constructor(
for (i in 0 until len) {
val codePoint = buf.getChar(offset + i * 2)
if (nullTerminated && codePoint == '0') {
if (nullTerminated && codePoint == '\u0000') {
break
}

View File

@ -5,7 +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.data.Store
import world.phantasmal.psoserv.encryption.BbCipher
import world.phantasmal.psoserv.encryption.Cipher
import world.phantasmal.psoserv.encryption.PcCipher
@ -74,8 +74,8 @@ fun main(args: Array<String>) {
}
// Initialize and start the server.
val accountStore = AccountStore(LOGGER)
val servers = initialize(config, accountStore)
val store = Store(LOGGER)
val servers = initialize(config, store)
if (start) {
if (servers.isEmpty()) {
@ -93,7 +93,7 @@ fun main(args: Array<String>) {
}
}
private fun initialize(config: Config, accountStore: AccountStore): List<Server> {
private fun initialize(config: Config, store: Store): List<Server> {
val address = config.address?.let(::inet4Address) ?: DEFAULT_ADDRESS
LOGGER.info { "Binding to $address." }
@ -184,7 +184,7 @@ private fun initialize(config: Config, accountStore: AccountStore): List<Server>
servers.add(
AccountServer(
accountStore,
store,
bindPair,
ships,
)
@ -213,7 +213,7 @@ private fun initialize(config: Config, accountStore: AccountStore): List<Server>
servers.add(
BlockServer(
accountStore,
store,
block.name,
block.bindPair,
blockId = index + 1,

View File

@ -1,108 +0,0 @@
package world.phantasmal.psoserv.data
import mu.KLogger
class AccountData(
private val logger: KLogger,
account: Account,
playing: PlayingAccount?,
private val password: String,
private var loggedIn: Boolean,
) {
/**
* All operations on this object must synchronize on this lock. All exposed data must be deeply
* immutable and represent a consistent snapshot of the object's state at the time of retrieval.
*/
private val lock = Any()
private var _account = account
private var _playing = playing
val account: Account get() = synchronized(lock) { _account }
val playing: PlayingAccount? get() = synchronized(lock) { _playing }
init {
require(password.length <= 16)
}
fun logIn(password: String): LogInResult =
synchronized(lock) {
if (password != this.password) {
LogInResult.BadPassword
} else if (loggedIn) {
LogInResult.AlreadyLoggedIn
} else {
loggedIn = true
LogInResult.Ok
}
}
fun logOut() {
synchronized(lock) {
if (!loggedIn) {
logger.warn {
"""Trying to log out account ${account.id} "${account.username}" while it wasn't logged in."""
}
}
_playing = null
loggedIn = false
}
}
fun setPlaying(char: Character, blockId: Int) {
synchronized(lock) {
_playing = PlayingAccount(account, char, blockId)
loggedIn = true
}
}
}
class Account(
val id: Long,
val username: String,
val guildCardNo: Int,
val teamId: Int,
val characters: List<Character>,
) {
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,
}
enum class LogInResult {
Ok, BadPassword, AlreadyLoggedIn
}

View File

@ -1,56 +0,0 @@
package world.phantasmal.psoserv.data
import mu.KLogger
class AccountStore(private val logger: KLogger) {
/**
* All operations on this object must synchronize on this lock.
*/
private val lock = Any()
private var nextId: Long = 1L
private var nextGuildCardNo: Int = 1
private val idToAccountData = mutableMapOf<Long, AccountData>()
private val usernameToAccountData = mutableMapOf<String, AccountData>()
fun getAccountData(username: String, password: String): AccountData =
synchronized(lock) {
// Simply create the account if it doesn't exist yet.
usernameToAccountData.getOrPut(username) {
val accountId = nextId++
AccountData(
logger = logger,
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,
).also {
// Ensure it can also be found by ID.
idToAccountData[accountId] = it
}
}
}
fun getPlayingAccountsForBlock(blockId: Int): List<PlayingAccount> =
synchronized(lock) {
idToAccountData.values.asSequence()
.mapNotNull { it.playing } // Map before filtering to avoid race condition.
.filter { it.blockId == blockId }
.toList()
}
}

View File

@ -0,0 +1,473 @@
package world.phantasmal.psoserv.data
import mu.KLogger
import world.phantasmal.psolib.Episode
import world.phantasmal.psoserv.messages.Message
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
/*
* Whenever data is changed in these classes, locks should be acquired in this order:
* 1. Store
* 2. LobbyOrParty
* 3. Client.
*/
private const val MAX_ACCOUNTS_PER_LOBBY: Int = 20
private const val MAX_ACCOUNTS_PER_PARTY: Int = 4
@RequiresOptIn(message = "This API is internal and should not be accessed from outside the file it was defined in.")
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION)
private annotation class Internal
// TODO: Periodically log out stale accounts.
// TODO: Periodically remove stale parties (lobbies too?).
@OptIn(Internal::class)
class Store(private val logger: KLogger) {
/**
* All operations on this object must synchronize on this lock.
*/
private val lock = Any()
private var nextAccountId: Long = 1L
private var nextCharId: Long = 1L
private var nextGuildCardNo: Int = 1
private val nameToClient = mutableMapOf<String, Client>()
private val blockIdToLobbyIdToLobby = mutableMapOf<Int, List<Lobby>>()
private var nextPartyId: Int = 1
private val blockIdToNameToParty = mutableMapOf<Int, MutableMap<String, Party>>()
fun authenticate(name: String, password: String, sendMessage: (Message) -> Unit): AuthResult {
val client = synchronized(lock) {
// Simply create the account and client if it doesn't exist yet.
nameToClient.getOrPut(name) {
val accountId = nextAccountId++
Client(
logger = logger,
account = Account(
id = accountId,
name = name,
guildCardNo = nextGuildCardNo++,
teamId = 1337,
characters = listOf(
Character(
id = nextCharId++,
accountId = accountId,
name = "${name.take(14)} 1",
sectionId = SectionId.Viridia,
exp = 1_000_000,
level = 200,
)
),
),
password = password,
)
}
}
return client.logIn(password, sendMessage)
}
fun logOut(client: Client) {
synchronized(lock) {
synchronizedClientAndLop(client) {
val lop = client.lop
if (lop is Party) {
lop.removeClient(client)
if (lop.isEmpty) {
val nameToParty = blockIdToNameToParty[lop.blockId]
?: return
nameToParty.remove(lop.details.name, lop)
}
}
client.logOut()
}
}
}
fun getLobbies(blockId: Int): List<Lobby> =
synchronized(lock) {
blockIdToLobbyIdToLobby.getOrPut(blockId) {
// Create lobbies if necessary.
// BlueBurst needs precisely 15 lobbies. It seems like they need to have IDs 0..14.
(0..14).map { lobbyId ->
Lobby(lobbyId, blockId)
}
}
}
/** Returns null if no lobbies are available or [client] is already in a lobby or party. */
fun joinFirstAvailableLobby(blockId: Int, client: Client): Lobby? =
synchronized(lock) {
val lobbies = getLobbies(blockId)
for (lobby in lobbies) {
synchronized(lobby) {
// Do unnecessary check here, so we don't have to lock on client everytime.
if (!lobby.isFull) {
synchronized(client.lock) {
if (client.lop != null) {
return null
}
val id = lobby.addClient(client)
// Can't be -1 at this point.
if (id.toInt() != -1) {
client.setLop(lobby, id)
return lobby
}
}
}
}
}
return null
}
/**
* Creates a new party and adds [leader] to it. Return null if a party with [name] already
* exists.
*/
fun createAndJoinParty(
blockId: Int,
name: String,
password: String,
difficulty: Difficulty,
episode: Episode,
mode: Mode,
leader: Client,
): CreateAndJoinPartyResult {
synchronized(lock) {
val nameToParty = blockIdToNameToParty.getOrPut(blockId, ::mutableMapOf)
if (name in nameToParty) {
return CreateAndJoinPartyResult.NameInUse
}
val details = PartySettings(name, password, difficulty, episode, mode)
val party = Party(nextPartyId++, blockId, details)
synchronized(party.lock) {
synchronizedClientAndLop(leader) {
if (leader.lop is Party) {
return CreateAndJoinPartyResult.AlreadyInParty
}
leader.lop?.removeClient(leader)
val id = party.addClient(leader)
check(id.toInt() != -1) { "Couldn't add client to newly created party." }
leader.setLop(party, id)
}
}
// Do this at the last possible time, so any exceptions and early returns can prevent
// the party from being added to the store.
nameToParty[name] = party
return CreateAndJoinPartyResult.Ok(party)
}
}
/**
* Acquires the LOP lock and then the client lock.
*/
private inline fun <T> synchronizedClientAndLop(
client: Client,
block: () -> T,
): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
while (true) {
val lop = client.lop
if (lop == null) {
synchronized(client.lock) {
if (client.lop == lop) {
return block()
}
// At this point we know LOP changed since last check, retry because we need to
// lock on LOP first.
}
} else {
synchronized(lop.lock) {
synchronized(client.lock) {
if (client.lop == lop) {
return block()
}
// At this point we know LOP changed since last check, we're holding the
// wrong LOP lock. Retry because we need to lock on LOP first.
}
}
}
}
}
}
@OptIn(Internal::class)
class Client(
private val logger: KLogger,
account: Account,
private val password: String,
) {
/**
* All operations on this object must synchronize on this lock.
*/
@Internal
val lock = Any()
private var _account: Account = account
private var _playing: PlayingAccount? = null
private var _lop: LobbyOrParty? = null
private var _id: Byte = -1
/** Non-null when logged in. */
private var sendMessage: ((Message) -> Unit)? = null
val account: Account get() = synchronized(lock) { _account }
val playing: PlayingAccount? get() = synchronized(lock) { _playing }
val lop: LobbyOrParty? get() = synchronized(lock) { _lop }
/**
* LOP-specific ID. -1 when not in a LOP.
*/
val id: Byte get() = synchronized(lock) { _id }
init {
require(password.length <= 16)
}
fun setPlaying(char: Character, blockId: Int) {
synchronized(lock) {
require(sendMessage != null) { "Trying to set a logged out account to playing." }
_playing = PlayingAccount(account, char, blockId)
}
}
fun sendMessage(message: Message) {
val snd = synchronized(lock) { sendMessage }
// Do blocking call outside synchronized block.
snd?.invoke(message)
}
@Internal
fun logIn(password: String, sendMessage: (Message) -> Unit): AuthResult =
synchronized(lock) {
if (password != this.password) {
AuthResult.BadPassword
} else if (this.sendMessage != null) {
AuthResult.AlreadyLoggedIn
} else {
this.sendMessage = sendMessage
AuthResult.Ok(this)
}
}
@Internal
fun logOut() {
synchronized(lock) {
if (sendMessage == null) {
logger.warn {
"""Trying to log out account ${account.id} "${account.name}" while it wasn't logged in."""
}
}
sendMessage = null
_playing = null
_lop = null
}
}
@Internal
fun setLop(lop: LobbyOrParty?, id: Byte) {
synchronized(lock) {
_id = id
_lop = lop
}
}
}
@OptIn(Internal::class)
sealed class LobbyOrParty(val id: Int, val blockId: Int, private val maxClients: Int) {
private var clientCount = 0
/**
* All operations on this object must synchronize on this lock. All exposed data must be deeply
* immutable and represent a consistent snapshot of the object's state at the time of retrieval.
*/
@Internal
val lock = Any()
private var _clients: MutableList<Client?> = MutableList(maxClients) { null }
private var _leaderId: Byte = -1
val isEmpty: Boolean get() = synchronized(lock) { clientCount == 0 }
val isFull: Boolean get() = synchronized(lock) { clientCount >= maxClients }
/** -1 If LOP has no clients. */
val leaderId: Byte get() = synchronized(lock) { _leaderId }
fun getClients(): List<Client> = synchronized(lock) { _clients.filterNotNull() }
fun broadcastMessage(message: Message, exclude: Client?) {
val clients = mutableListOf<Client>()
synchronized(lock) {
for (client in _clients) {
if (client != null && client != exclude) {
clients.add(client)
}
}
}
// Do blocking calls outside of synchronized block.
for (client in clients) {
client.sendMessage(message)
}
}
/**
* Returns the ID of the client within this lobby when the client can be added, -1 otherwise.
*/
@Internal
fun addClient(client: Client): Byte {
synchronized(lock) {
val iter = _clients.listIterator()
while (iter.hasNext()) {
val id = iter.nextIndex().toByte()
if (iter.next() == null) {
iter.set(client)
if (clientCount == 0) {
_leaderId = id
}
clientCount++
return id
}
}
}
return -1
}
@Internal
fun removeClient(client: Client) {
synchronized(lock) {
// Find the client's ID ourselves, so we don't need to lock on client. This way we also
// don't have to trust client.
val id = _clients.indexOf(client)
if (id != -1) {
_clients[id] = null
clientCount--
if (clientCount == 0) {
_leaderId = -1
}
if (id == _leaderId.toInt()) {
_leaderId = _clients.firstNotNullOfOrNull { it }?.id ?: -1
}
}
}
}
}
class Lobby(id: Int, blockId: Int) : LobbyOrParty(id, blockId, MAX_ACCOUNTS_PER_LOBBY)
@OptIn(Internal::class)
class Party(id: Int, blockId: Int, details: PartySettings) :
LobbyOrParty(id, blockId, MAX_ACCOUNTS_PER_PARTY) {
private var _details: PartySettings = details
val details: PartySettings get() = synchronized(lock) { _details }
}
class Account(
val id: Long,
val name: String,
val guildCardNo: Int,
val teamId: Int,
val characters: List<Character>,
) {
init {
require(name.length <= 16)
}
override fun toString(): String = "Account[id=$id,name=$name]"
}
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,
}
class PartySettings(
val name: String,
val password: String,
val difficulty: Difficulty,
val episode: Episode,
val mode: Mode,
)
enum class Difficulty {
Normal, Hard, VHard, Ultimate
}
enum class Mode {
Normal, Battle, Challenge, Solo
}
sealed class AuthResult {
class Ok(val client: Client) : AuthResult()
object BadPassword : AuthResult()
object AlreadyLoggedIn : AuthResult()
}
sealed class CreateAndJoinPartyResult {
class Ok(val party: Party) : CreateAndJoinPartyResult()
object NameInUse : CreateAndJoinPartyResult()
object AlreadyInParty : CreateAndJoinPartyResult()
}

View File

@ -36,7 +36,9 @@ object BbMessageDescriptor : MessageDescriptor<BbMessage> {
0x001D -> BbMessage.Ping(buffer)
0x0060 -> BbMessage.Broadcast(buffer)
0x0061 -> BbMessage.CharData(buffer)
0x0064 -> BbMessage.JoinParty(buffer)
0x0067 -> BbMessage.JoinLobby(buffer)
0x0068 -> BbMessage.JoinedLobby(buffer)
0x0083 -> BbMessage.LobbyList(buffer)
0x0093 -> BbMessage.Authenticate(buffer)
0x0095 -> BbMessage.GetCharData(buffer)
@ -167,7 +169,7 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_
// 0x001D
class Ping(buffer: Buffer) : BbMessage(buffer) {
constructor() : this(buf(code = 0x001D))
constructor() : this(buf(0x001D))
}
// 0x0060
@ -193,8 +195,51 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_
// 0x0061
class CharData(buffer: Buffer) : BbMessage(buffer)
// 0x0067
class JoinLobby(buffer: Buffer) : BbMessage(buffer) {
// 0x0064
class JoinParty(buffer: Buffer) : BbMessage(buffer) {
constructor(
players: List<LobbyPlayer>,
clientId: Byte,
leaderId: Byte,
difficulty: Byte,
battleMode: Boolean,
event: Byte,
sectionId: Byte,
challengeMode: Boolean,
prngSeed: Int,
episode: Byte,
soloMode: Boolean,
) : this(
buf(0x0064, 416, flags = players.size) {
require(players.size <= 4)
repeat(32) { writeInt(0) } // Map layout
for (player in players) {
player.write(this)
}
// Empty player slots.
repeat((4 - players.size) * (LobbyPlayer.SIZE / 4)) { writeInt(0) }
writeByte(clientId)
writeByte(leaderId)
writeByte(1) // Unknown
writeByte(difficulty)
writeByte(if (battleMode) 1 else 0)
writeByte(event)
writeByte(sectionId)
writeByte(if (challengeMode) 1 else 0)
writeInt(prngSeed)
writeByte(episode)
writeByte(1) // Unknown
writeByte(if (soloMode) 1 else 0)
writeByte(0) // Unused
}
)
}
abstract class AbstractJoinLobby(buffer: Buffer) : BbMessage(buffer) {
val playerCount: Int get() = flags
var clientId: UByte
get() = uByte(0)
@ -209,39 +254,93 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_
get() = uShort(4)
set(value) = setUShort(4, value)
override fun toString(): String =
messageString(
"playerCount" to playerCount,
"clientId" to clientId,
"leaderId" to leaderId,
"lobbyNo" to lobbyNo,
"blockNo" to blockNo,
)
companion object {
@JvmStatic
protected fun joinLobbyBuf(
code: Int,
clientId: Byte,
leaderId: Byte,
disableUdp: Boolean,
lobbyNo: UByte,
blockNo: UShort,
event: UShort,
players: List<LobbyPlayer>,
): Buffer =
buf(code, 12 + players.size * (LobbyPlayer.SIZE + 1244), flags = players.size) {
writeByte(clientId)
writeByte(leaderId)
writeByte(if (disableUdp) 1 else 0)
writeUByte(lobbyNo)
writeUShort(blockNo)
writeUShort(event)
writeInt(0) // Unused.
for (player in players) {
player.write(this)
repeat(311) { writeInt(0) } // Inventory and character data.
}
}
}
}
// 0x0067
class JoinLobby(buffer: Buffer) : AbstractJoinLobby(buffer) {
constructor(
clientId: UByte,
leaderId: UByte,
clientId: Byte,
leaderId: Byte,
disableUdp: Boolean,
lobbyNo: UByte,
blockNo: UShort,
event: UShort,
players: List<LobbyPlayer>,
) : this(
buf(0x0067, 12 + players.size * 1312, flags = players.size) {
writeUByte(clientId)
writeUByte(leaderId)
writeByte(if (disableUdp) 1 else 0)
writeUByte(lobbyNo)
writeUShort(blockNo)
writeUShort(event)
writeInt(0) // Unused.
joinLobbyBuf(
0x0067,
clientId,
leaderId,
disableUdp,
lobbyNo,
blockNo,
event,
players,
)
)
}
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) }
}
}
// 0x0068
class JoinedLobby(buffer: Buffer) : AbstractJoinLobby(buffer) {
constructor(
clientId: Byte,
leaderId: Byte,
disableUdp: Boolean,
lobbyNo: UByte,
blockNo: UShort,
event: UShort,
player: LobbyPlayer,
) : this(
joinLobbyBuf(
0x0068,
clientId,
leaderId,
disableUdp,
lobbyNo,
blockNo,
event,
listOf(player),
)
)
override fun toString(): String =
messageString(
"playerCount" to playerCount,
"clientId" to clientId,
"leaderId" to leaderId,
"lobbyNo" to lobbyNo,
@ -251,11 +350,11 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_
// 0x0083
class LobbyList(buffer: Buffer) : BbMessage(buffer) {
constructor() : this(
constructor(lobbyIds: List<Int>) : this(
buf(0x0083, 192) {
repeat(15) {
for (lobbyId in lobbyIds) {
writeInt(MenuType.Lobby.toInt())
writeInt(it + 1) // Item ID.
writeInt(lobbyId) // Item ID.
writeInt(0) // Padding.
}
// 12 zero bytes of padding.
@ -277,6 +376,16 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_
val magic: Int get() = int(132) // Should be 0xDEADBEEF
val charSlot: Int get() = byte(136).toInt()
val charSelected: Boolean get() = byte(137).toInt() != 0
override fun toString(): String =
messageString(
"guildCardNo" to guildCardNo,
"version" to version,
"teamId" to teamId,
"username" to username,
"charSlot" to charSlot,
"charSelected" to charSelected,
)
}
// 0x0095
@ -305,7 +414,39 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_
}
// 0x00C1
class CreateParty(buffer: Buffer) : BbMessage(buffer)
class CreateParty(buffer: Buffer) : BbMessage(buffer) {
var name: String
get() = stringUtf16(8, 32)
set(value) = setStringUtf16(8, value, 32)
var password: String
get() = stringUtf16(40, 32)
set(value) = setStringUtf16(40, value, 32)
var difficulty: Byte
get() = byte(72)
set(value) = setByte(72, value)
var battleMode: Boolean
get() = byte(73).toInt() != 0
set(value) = setByte(73, if (value) 1 else 0)
var challengeMode: Boolean
get() = byte(74).toInt() != 0
set(value) = setByte(74, if (value) 1 else 0)
var episode: Byte
get() = byte(75)
set(value) = setByte(75, value)
var soloMode: Boolean
get() = byte(76).toInt() != 0
set(value) = setByte(76, if (value) 1 else 0)
override fun toString(): String =
messageString(
"name" to name,
"difficulty" to difficulty,
"battleMode" to battleMode,
"challengeMode" to challengeMode,
"episode" to episode,
"soloMode" to soloMode,
)
}
// 0x01DC
class GuildCardHeader(buffer: Buffer) : BbMessage(buffer) {
@ -735,9 +876,49 @@ class PsoCharData(
}
}
class PlayerHeader(
val playerTag: Int,
val guildCardNo: Int,
val clientId: UByte,
val charName: String,
) {
fun write(cursor: WritableCursor) {
with(cursor) {
writeInt(playerTag)
writeInt(guildCardNo)
repeat(5) { writeInt(0) } // Unknown.
writeUByte(clientId)
repeat(3) { writeByte(0) } // Unknown.
writeStringUtf16(charName, 32)
writeInt(0) // Unknown.
repeat(311) { writeInt(0) }
}
}
companion object {
const val SIZE: Int = 1312
}
}
class LobbyPlayer(
val playerTag: Int,
val guildCardNo: Int,
val clientId: Int,
val clientId: Byte,
val charName: String,
)
) {
fun write(cursor: WritableCursor) {
with(cursor) {
writeInt(playerTag)
writeInt(guildCardNo)
repeat(5) { writeInt(0) } // Unknown.
writeByte(clientId)
repeat(3) { writeByte(0) } // Unknown.
writeStringUtf16(charName, 32)
writeInt(0) // Unknown.
}
}
companion object {
const val SIZE: Int = 68
}
}

View File

@ -72,6 +72,8 @@ abstract class AbstractMessage(override val headerSize: Int) : Message {
protected fun byteArray(offset: Int, size: Int) = ByteArray(size) { byte(offset + it) }
protected fun stringAscii(offset: Int, maxByteLength: Int) =
buffer.getStringAscii(headerSize + offset, maxByteLength, nullTerminated = true)
protected fun stringUtf16(offset: Int, maxByteLength: Int) =
buffer.getStringUtf16(headerSize + offset, maxByteLength, nullTerminated = true)
protected fun setUByte(offset: Int, value: UByte) {
buffer.setUByte(headerSize + offset, value)
@ -103,6 +105,10 @@ abstract class AbstractMessage(override val headerSize: Int) : Message {
buffer.setStringAscii(headerSize + offset, str, byteLength)
}
protected fun setStringUtf16(offset: Int, str: String, byteLength: Int) {
buffer.setStringUtf16(headerSize + offset, str, byteLength)
}
protected fun messageString(vararg props: Pair<String, Any>): String =
messageString(code, size, flags, this::class.simpleName, *props)
}

View File

@ -4,16 +4,16 @@ 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.AccountData
import world.phantasmal.psoserv.data.AccountStore
import world.phantasmal.psoserv.data.LogInResult
import world.phantasmal.psoserv.data.Client
import world.phantasmal.psoserv.data.AuthResult
import world.phantasmal.psoserv.data.Store
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,
private val store: Store,
bindPair: Inet4Pair,
private val ships: List<ShipInfo>,
) : GameServer<BbMessage>("account", bindPair) {
@ -27,7 +27,7 @@ class AccountServer(
serverCipher: Cipher,
clientCipher: Cipher,
): ClientReceiver<BbMessage> = object : ClientReceiver<BbMessage> {
private var accountData: AccountData? = null
private var client: Client? = null
private val guildCardBuffer = Buffer.withSize(54672)
private var fileChunkNo = 0
private var charSlot: Int = 0
@ -35,15 +35,19 @@ class AccountServer(
override fun process(message: BbMessage): Boolean = when (message) {
is BbMessage.Authenticate -> {
val accountData = accountStore.getAccountData(message.username, message.password)
this.accountData = accountData
val result = store.authenticate(
message.username,
message.password,
ctx::send,
)
when (accountData.logIn(message.password)) {
LogInResult.Ok -> {
when (result) {
is AuthResult.Ok -> {
client = result.client
charSlot = message.charSlot
charSelected = message.charSelected
val account = accountData.account
val account = result.client.account
ctx.send(
BbMessage.AuthData(
AuthStatus.Success,
@ -60,7 +64,7 @@ class AccountServer(
ctx.send(BbMessage.ShipList(ships.map { it.uiName }))
}
}
LogInResult.BadPassword -> {
AuthResult.BadPassword -> {
ctx.send(
BbMessage.AuthData(
AuthStatus.Nonexistent,
@ -71,7 +75,7 @@ class AccountServer(
)
)
}
LogInResult.AlreadyLoggedIn -> {
AuthResult.AlreadyLoggedIn -> {
ctx.send(
BbMessage.AuthData(
AuthStatus.Error,
@ -88,7 +92,7 @@ class AccountServer(
}
is BbMessage.GetAccount -> {
accountData?.account?.let {
client?.account?.let {
ctx.send(BbMessage.Account(it.guildCardNo, it.teamId))
}
@ -96,7 +100,7 @@ class AccountServer(
}
is BbMessage.CharSelect -> {
val account = accountData?.account
val account = client?.account
if (account != null && message.slot in account.characters.indices) {
if (message.selected) {
@ -252,9 +256,9 @@ class AccountServer(
private fun logOut() {
try {
accountData?.let(AccountData::logOut)
client?.let(store::logOut)
} finally {
accountData = null
client = null
}
}
}

View File

@ -1,14 +1,13 @@
package world.phantasmal.psoserv.servers
import world.phantasmal.psoserv.data.AccountData
import world.phantasmal.psoserv.data.AccountStore
import world.phantasmal.psoserv.data.LogInResult
import world.phantasmal.psolib.Episode
import world.phantasmal.psoserv.data.*
import world.phantasmal.psoserv.encryption.BbCipher
import world.phantasmal.psoserv.encryption.Cipher
import world.phantasmal.psoserv.messages.*
class BlockServer(
private val accountStore: AccountStore,
private val store: Store,
name: String,
bindPair: Inet4Pair,
private val blockId: Int,
@ -23,18 +22,67 @@ class BlockServer(
serverCipher: Cipher,
clientCipher: Cipher,
): ClientReceiver<BbMessage> = object : ClientReceiver<BbMessage> {
private var accountData: AccountData? = null
private var client: Client? = null
override fun process(message: BbMessage): Boolean = when (message) {
is BbMessage.Authenticate -> {
val accountData = accountStore.getAccountData(message.username, message.password)
this.accountData = accountData
override fun process(message: BbMessage): Boolean {
when (message) {
is BbMessage.Authenticate -> {
val result = store.authenticate(
message.username,
message.password,
ctx::send,
)
when (accountData.logIn(message.password)) {
LogInResult.Ok -> {
val char = accountData.account.characters.getOrNull(message.charSlot)
when (result) {
is AuthResult.Ok -> {
client = result.client
if (char == null) {
val account = result.client.account
val char = account.characters.getOrNull(message.charSlot)
if (char == null) {
ctx.send(
BbMessage.AuthData(
AuthStatus.Nonexistent,
message.guildCardNo,
message.teamId,
message.charSlot,
message.charSelected,
)
)
} else {
result.client.setPlaying(char, blockId)
ctx.send(
BbMessage.AuthData(
AuthStatus.Success,
account.guildCardNo,
account.teamId,
message.charSlot,
message.charSelected,
)
)
val lobbies = store.getLobbies(blockId)
ctx.send(BbMessage.LobbyList(lobbies.map { it.id }))
ctx.send(
BbMessage.FullCharacterData(
// TODO: Fill in char data correctly.
PsoCharData(
hp = 0,
level = char.level - 1,
exp = char.exp,
sectionId = char.sectionId.ordinal.toByte(),
charClass = 0,
name = char.name,
),
)
)
ctx.send(BbMessage.GetCharData())
}
}
AuthResult.BadPassword -> {
ctx.send(
BbMessage.AuthData(
AuthStatus.Nonexistent,
@ -44,96 +92,178 @@ class BlockServer(
message.charSelected,
)
)
} else {
accountData.setPlaying(char, blockId)
val account = accountData.account
}
AuthResult.AlreadyLoggedIn -> {
ctx.send(
BbMessage.AuthData(
AuthStatus.Success,
account.guildCardNo,
account.teamId,
AuthStatus.Error,
message.guildCardNo,
message.teamId,
message.charSlot,
message.charSelected,
)
)
ctx.send(BbMessage.LobbyList())
ctx.send(
BbMessage.FullCharacterData(
PsoCharData(
hp = 0,
level = char.level - 1,
exp = char.exp,
sectionId = char.sectionId.ordinal.toByte(),
charClass = 0,
name = char.name,
),
)
)
ctx.send(BbMessage.GetCharData())
}
}
LogInResult.BadPassword -> {
ctx.send(
BbMessage.AuthData(
AuthStatus.Nonexistent,
message.guildCardNo,
message.teamId,
message.charSlot,
message.charSelected,
)
return true
}
is BbMessage.CharData -> {
val client = client
?: return false
val lobby = store.joinFirstAvailableLobby(blockId, client)
?: return false
val clientId = client.id
ctx.send(
BbMessage.JoinLobby(
clientId = clientId,
leaderId = 0, // TODO: What should leaderId be in lobbies?
disableUdp = true,
lobbyNo = lobby.id.toUByte(),
blockNo = blockId.toUShort(),
event = 0u,
players = lobby.getClients().mapNotNull { c ->
c.playing?.let {
LobbyPlayer(
playerTag = 0,
guildCardNo = it.account.guildCardNo,
clientId = c.id,
charName = it.char.name,
)
}
}
)
)
// Notify other clients.
client.playing?.let { playingAccount ->
val joinedMessage = BbMessage.JoinedLobby(
clientId = clientId,
leaderId = 0, // TODO: What should leaderId be in lobbies?
disableUdp = true,
lobbyNo = lobby.id.toUByte(),
blockNo = blockId.toUShort(),
event = 0u,
player = LobbyPlayer(
playerTag = 0,
guildCardNo = playingAccount.account.guildCardNo,
clientId = clientId,
charName = playingAccount.char.name,
),
)
lobby.broadcastMessage(joinedMessage, exclude = client)
}
LogInResult.AlreadyLoggedIn -> {
ctx.send(
BbMessage.AuthData(
AuthStatus.Error,
message.guildCardNo,
message.teamId,
message.charSlot,
message.charSelected,
)
)
return true
}
is BbMessage.CreateParty -> {
val client = client
?: return false
val difficulty = when (message.difficulty.toInt()) {
0 -> Difficulty.Normal
1 -> Difficulty.Hard
2 -> Difficulty.VHard
3 -> Difficulty.Ultimate
else -> return false
}
val episode = when (message.episode.toInt()) {
1 -> Episode.I
2 -> Episode.II
3 -> Episode.IV
else -> return false
}
val mode = when {
message.battleMode -> Mode.Battle
message.challengeMode -> Mode.Challenge
message.soloMode -> Mode.Solo
else -> Mode.Normal
}
val result = store.createAndJoinParty(
blockId,
message.name,
message.password,
difficulty,
episode,
mode,
client,
)
when (result) {
is CreateAndJoinPartyResult.Ok -> {
val party = result.party
val details = party.details
// TODO: Send lobby leave message to all clients.
ctx.send(BbMessage.JoinParty(
players = party.getClients().mapNotNull { c ->
c.playing?.let {
LobbyPlayer(
playerTag = 0,
guildCardNo = it.account.guildCardNo,
clientId = c.id,
charName = it.char.name,
)
}
},
clientId = client.id,
leaderId = party.leaderId,
difficulty = when (details.difficulty) {
Difficulty.Normal -> 0
Difficulty.Hard -> 1
Difficulty.VHard -> 2
Difficulty.Ultimate -> 3
},
battleMode = details.mode == Mode.Battle,
event = 0,
sectionId = 0,
challengeMode = details.mode == Mode.Challenge,
prngSeed = 0,
episode = when (details.episode) {
Episode.I -> 1
Episode.II -> 2
Episode.IV -> 3
},
soloMode = details.mode == Mode.Solo,
))
// TODO: Send player join message to other clients.
return true
}
is CreateAndJoinPartyResult.NameInUse -> {
// TODO: Just send message instead of disconnecting.
return false
}
is CreateAndJoinPartyResult.AlreadyInParty -> {
logger.warn {
"${client.account} tried to create a party while in a party."
}
return true
}
}
}
true
is BbMessage.Broadcast -> {
// TODO: Verify broadcast messages.
client?.lop?.broadcastMessage(message, client)
return true
}
is BbMessage.Disconnect -> {
// Log out and disconnect.
logOut()
return false
}
else -> return ctx.unexpectedMessage(message)
}
is BbMessage.CharData -> {
ctx.send(
BbMessage.JoinLobby(
clientId = 0u,
leaderId = 0u,
disableUdp = true,
lobbyNo = 0u,
blockNo = blockId.toUShort(),
event = 0u,
players = accountStore.getPlayingAccountsForBlock(blockId).map {
LobbyPlayer(
playerTag = 0,
guildCardNo = it.account.guildCardNo,
clientId = 0,
charName = it.char.name,
)
}
)
)
true
}
is BbMessage.CreateParty -> {
true
}
is BbMessage.Disconnect -> {
// Log out and disconnect.
logOut()
false
}
else -> ctx.unexpectedMessage(message)
}
override fun connectionClosed() {
@ -142,9 +272,9 @@ class BlockServer(
private fun logOut() {
try {
accountData?.let(AccountData::logOut)
client?.let(store::logOut)
} finally {
accountData = null
client = null
}
}
}

View File

@ -40,7 +40,7 @@ abstract class GameServer<MessageType : Message>(
private val logger: KLogger,
private val handler: SocketHandler<MessageType>,
) {
fun send(message: MessageType, encrypt: Boolean = true) {
fun send(message: Message, encrypt: Boolean = true) {
handler.sendMessage(message, encrypt)
}

View File

@ -49,6 +49,7 @@ abstract class Server(
while (running) {
try {
val clientSocket = bindSocket.accept()
// TODO: Limit number of connected clients.
logger.info {
"New client connection from ${clientSocket.inetAddress}:${clientSocket.port}."
}

View File

@ -140,8 +140,8 @@ abstract class SocketHandler<MessageType : Message>(
break@readLoop
}
}
} catch (e: Throwable) {
logger.error(e) { "Error while processing message." }
} catch (e: Exception) {
logger.error(e) { "Exception while processing message." }
}
offset += encryptedSize
@ -231,19 +231,14 @@ abstract class SocketHandler<MessageType : Message>(
socket.close()
}
fun sendMessage(message: MessageType, encrypt: Boolean) {
fun sendMessage(message: Message, encrypt: Boolean) {
logger.trace {
"Sending $message${if (encrypt) "" else " (unencrypted)"}."
}
if (message.buffer.size != message.size) {
logger.warn {
"Message size of $message is ${message.size}B, but wrote ${message.buffer.size} bytes."
}
}
val cipher = writeEncryptCipher
val buffer: Buffer
val expectedMaxSize: Int
if (encrypt) {
checkNotNull(cipher)
@ -253,8 +248,17 @@ abstract class SocketHandler<MessageType : Message>(
size = alignToWidth(initialSize, cipher.blockSize)
)
cipher.encrypt(buffer)
expectedMaxSize = alignToWidth(message.size, cipher.blockSize)
} else {
buffer = message.buffer
expectedMaxSize = message.size
}
// Message buffer can be padded for encryption in advance.
if (message.buffer.size !in message.size..expectedMaxSize) {
logger.warn {
"Message size of $message is ${message.size}B, but wrote ${message.buffer.size} bytes."
}
}
socket.write(buffer, 0, buffer.size)