mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-03 13:58:28 +08:00
Added lobbies and party creation. Broadcast messages are now broadcast to other clients (without verification).
This commit is contained in:
parent
b038851a29
commit
c2f50e6827
@ -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
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
473
psoserv/src/main/kotlin/world/phantasmal/psoserv/data/Data.kt
Normal file
473
psoserv/src/main/kotlin/world/phantasmal/psoserv/data/Data.kt
Normal 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()
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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}."
|
||||
}
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user