mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 22:58:29 +08:00
Simplified psoserv architecture.
This commit is contained in:
parent
f6e0ed06a9
commit
810c1cb549
@ -11,9 +11,6 @@ import world.phantasmal.psoserv.messages.Message
|
||||
import world.phantasmal.psoserv.messages.MessageDescriptor
|
||||
import world.phantasmal.psoserv.messages.PcMessageDescriptor
|
||||
import world.phantasmal.psoserv.servers.*
|
||||
import world.phantasmal.psoserv.servers.account.AccountServer
|
||||
import world.phantasmal.psoserv.servers.auth.AuthServer
|
||||
import world.phantasmal.psoserv.servers.patch.PatchServer
|
||||
import java.io.File
|
||||
import java.net.Inet4Address
|
||||
|
||||
@ -21,7 +18,7 @@ import java.net.Inet4Address
|
||||
private val DEFAULT_ADDRESS: Inet4Address = inet4Loopback()
|
||||
private const val DEFAULT_PATCH_PORT: Int = 11_000
|
||||
private const val DEFAULT_LOGIN_PORT: Int = 12_000
|
||||
private const val DEFAULT_DATA_PORT: Int = 12_001
|
||||
private const val DEFAULT_ACCOUNT_PORT: Int = 12_001
|
||||
|
||||
private val LOGGER = KotlinLogging.logger("main")
|
||||
|
||||
@ -87,8 +84,8 @@ private class PhantasmalServer(
|
||||
private fun initialize(config: Config): PhantasmalServer {
|
||||
val defaultAddress = config.address?.let(::inet4Address) ?: DEFAULT_ADDRESS
|
||||
|
||||
val dataAddress = config.account?.address?.let(::inet4Address) ?: defaultAddress
|
||||
val dataPort = config.account?.port ?: DEFAULT_DATA_PORT
|
||||
val accountAddress = config.account?.address?.let(::inet4Address) ?: defaultAddress
|
||||
val accountPort = config.account?.port ?: DEFAULT_ACCOUNT_PORT
|
||||
|
||||
val servers = mutableListOf<Server>()
|
||||
|
||||
@ -124,8 +121,8 @@ private fun initialize(config: Config): PhantasmalServer {
|
||||
AuthServer(
|
||||
name = "auth",
|
||||
bindPair,
|
||||
dataServerAddress = dataAddress,
|
||||
dataServerPort = dataPort,
|
||||
accountServerAddress = accountAddress,
|
||||
accountServerPort = accountPort,
|
||||
)
|
||||
)
|
||||
}
|
||||
@ -133,7 +130,7 @@ private fun initialize(config: Config): PhantasmalServer {
|
||||
if (config.account == null && run || config.account?.run == true) {
|
||||
val bindPair = Inet4Pair(
|
||||
config.account?.address?.let(::inet4Address) ?: defaultAddress,
|
||||
config.account?.port ?: DEFAULT_DATA_PORT,
|
||||
config.account?.port ?: DEFAULT_ACCOUNT_PORT,
|
||||
)
|
||||
|
||||
LOGGER.info { "Configuring account server to bind to $bindPair." }
|
||||
|
@ -0,0 +1,241 @@
|
||||
package world.phantasmal.psoserv.servers
|
||||
|
||||
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.encryption.BbCipher
|
||||
import world.phantasmal.psoserv.encryption.Cipher
|
||||
import world.phantasmal.psoserv.messages.*
|
||||
import world.phantasmal.psoserv.utils.crc32Checksum
|
||||
|
||||
class AccountServer(
|
||||
name: String,
|
||||
bindPair: Inet4Pair,
|
||||
) : GameServer<BbMessage>(name, bindPair) {
|
||||
|
||||
override val messageDescriptor = BbMessageDescriptor
|
||||
|
||||
override fun createCipher() = BbCipher()
|
||||
|
||||
override fun createClientReceiver(
|
||||
sender: ClientSender<BbMessage>,
|
||||
serverCipher: Cipher,
|
||||
clientCipher: Cipher,
|
||||
): ClientReceiver<BbMessage> = object : ClientReceiver<BbMessage> {
|
||||
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 charSelected: Boolean = false
|
||||
|
||||
init {
|
||||
sender.send(
|
||||
BbMessage.InitEncryption(
|
||||
"Phantasy Star Online Blue Burst Game Server. Copyright 1999-2004 SONICTEAM.",
|
||||
serverCipher.key,
|
||||
clientCipher.key,
|
||||
),
|
||||
encrypt = false,
|
||||
)
|
||||
}
|
||||
|
||||
override fun process(message: BbMessage): Boolean = when (message) {
|
||||
is BbMessage.Authenticate -> {
|
||||
// TODO: Actual authentication.
|
||||
guildCard = message.guildCard
|
||||
teamId = message.teamId
|
||||
send(
|
||||
BbMessage.AuthData(
|
||||
BbAuthStatus.Success,
|
||||
guildCard,
|
||||
teamId,
|
||||
slot,
|
||||
charSelected,
|
||||
)
|
||||
)
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
is BbMessage.GetAccount -> {
|
||||
// TODO: Send correct guild card number and team ID.
|
||||
send(BbMessage.Account(0, 0))
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
is BbMessage.CharSelect -> {
|
||||
if (message.select) {
|
||||
// Player has chosen a character.
|
||||
// TODO: Verify slot.
|
||||
if (slot in 0..3) {
|
||||
slot = message.slot
|
||||
charSelected = true
|
||||
send(
|
||||
BbMessage.AuthData(
|
||||
BbAuthStatus.Success,
|
||||
guildCard,
|
||||
teamId,
|
||||
slot,
|
||||
charSelected,
|
||||
)
|
||||
)
|
||||
send(
|
||||
BbMessage.CharSelectAck(slot, BbCharSelectStatus.Select)
|
||||
)
|
||||
} else {
|
||||
send(
|
||||
BbMessage.CharSelectAck(slot, BbCharSelectStatus.Nonexistent)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Player is previewing characters.
|
||||
// TODO: Look up character data.
|
||||
send(
|
||||
BbMessage.CharData(
|
||||
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,
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
is BbMessage.Checksum -> {
|
||||
// TODO: Checksum checking.
|
||||
send(BbMessage.ChecksumAck(true))
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
is BbMessage.GetGuildCardHeader -> {
|
||||
send(
|
||||
BbMessage.GuildCardHeader(
|
||||
guildCardBuffer.size,
|
||||
crc32Checksum(guildCardBuffer),
|
||||
)
|
||||
)
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
is BbMessage.GetGuildCardChunk -> {
|
||||
if (message.cont) {
|
||||
val offset = clamp(
|
||||
message.chunkNo * MAX_CHUNK_SIZE,
|
||||
min = 0,
|
||||
max = guildCardBuffer.size,
|
||||
)
|
||||
val size = (guildCardBuffer.size - offset).coerceAtMost(MAX_CHUNK_SIZE)
|
||||
|
||||
send(
|
||||
BbMessage.GuildCardChunk(
|
||||
message.chunkNo,
|
||||
guildCardBuffer.cursor(offset, size),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
is BbMessage.GetFileList -> {
|
||||
fileChunkNo = 0
|
||||
|
||||
send(BbMessage.FileList(FILE_LIST))
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
is BbMessage.GetFileChunk -> {
|
||||
val offset = (fileChunkNo * MAX_CHUNK_SIZE).coerceAtMost(
|
||||
FILE_BUFFER.size
|
||||
)
|
||||
val size = (FILE_BUFFER.size - offset).coerceAtMost(
|
||||
MAX_CHUNK_SIZE
|
||||
)
|
||||
|
||||
send(BbMessage.FileChunk(fileChunkNo, FILE_BUFFER.cursor(offset, size)))
|
||||
|
||||
if (offset + size < FILE_BUFFER.size) {
|
||||
fileChunkNo++
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
is BbMessage.Disconnect -> false
|
||||
|
||||
else -> unexpectedMessage(message)
|
||||
}
|
||||
|
||||
private fun send(message: BbMessage) {
|
||||
sender.send(message)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_CHUNK_SIZE: Int = 0x6800
|
||||
private val FILE_LIST: List<FileListEntry>
|
||||
private val FILE_BUFFER: Buffer
|
||||
|
||||
init {
|
||||
val filenames = listOf(
|
||||
"BattleParamEntry.dat",
|
||||
"BattleParamEntry_ep4.dat",
|
||||
"BattleParamEntry_ep4_on.dat",
|
||||
"BattleParamEntry_lab.dat",
|
||||
"BattleParamEntry_lab_on.dat",
|
||||
"BattleParamEntry_on.dat",
|
||||
"ItemMagEdit.prs",
|
||||
"ItemPMT.prs",
|
||||
"PlyLevelTbl.prs",
|
||||
)
|
||||
val fileBuffers = mutableListOf<Buffer>()
|
||||
|
||||
val fileList = mutableListOf<FileListEntry>()
|
||||
var offset = 0
|
||||
|
||||
for (filename in filenames) {
|
||||
val data = Buffer.fromResource("/world/phantasmal/psoserv/$filename")
|
||||
fileList.add(FileListEntry(data.size, crc32Checksum(data), offset, filename))
|
||||
fileBuffers.add(data)
|
||||
offset += data.size
|
||||
}
|
||||
|
||||
FILE_LIST = fileList
|
||||
|
||||
FILE_BUFFER = Buffer.withSize(offset, Endianness.Little)
|
||||
offset = 0
|
||||
|
||||
for (data in fileBuffers) {
|
||||
data.copyInto(FILE_BUFFER, destinationOffset = offset)
|
||||
offset += data.size
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
package world.phantasmal.psoserv.servers
|
||||
|
||||
import world.phantasmal.psoserv.encryption.BbCipher
|
||||
import world.phantasmal.psoserv.encryption.Cipher
|
||||
import world.phantasmal.psoserv.messages.BbAuthStatus
|
||||
import world.phantasmal.psoserv.messages.BbMessage
|
||||
import world.phantasmal.psoserv.messages.BbMessageDescriptor
|
||||
import java.net.Inet4Address
|
||||
|
||||
class AuthServer(
|
||||
name: String,
|
||||
bindPair: Inet4Pair,
|
||||
private val accountServerAddress: Inet4Address,
|
||||
private val accountServerPort: Int,
|
||||
) : GameServer<BbMessage>(name, bindPair) {
|
||||
|
||||
override val messageDescriptor = BbMessageDescriptor
|
||||
|
||||
override fun createCipher() = BbCipher()
|
||||
|
||||
override fun createClientReceiver(
|
||||
sender: ClientSender<BbMessage>,
|
||||
serverCipher: Cipher,
|
||||
clientCipher: Cipher,
|
||||
): ClientReceiver<BbMessage> = object : ClientReceiver<BbMessage> {
|
||||
init {
|
||||
sender.send(
|
||||
BbMessage.InitEncryption(
|
||||
"Phantasy Star Online Blue Burst Game Server. Copyright 1999-2004 SONICTEAM.",
|
||||
serverCipher.key,
|
||||
clientCipher.key,
|
||||
),
|
||||
encrypt = false,
|
||||
)
|
||||
}
|
||||
|
||||
override fun process(message: BbMessage): Boolean = when (message) {
|
||||
is BbMessage.Authenticate -> {
|
||||
// TODO: Actual authentication.
|
||||
send(
|
||||
BbMessage.AuthData(
|
||||
BbAuthStatus.Success,
|
||||
message.guildCard,
|
||||
message.teamId,
|
||||
slot = 0,
|
||||
selected = false,
|
||||
)
|
||||
)
|
||||
send(
|
||||
BbMessage.Redirect(accountServerAddress.address, accountServerPort)
|
||||
)
|
||||
|
||||
// Disconnect.
|
||||
false
|
||||
}
|
||||
|
||||
else -> unexpectedMessage(message)
|
||||
}
|
||||
|
||||
private fun send(message: BbMessage) {
|
||||
sender.send(message)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
package world.phantasmal.psoserv.servers
|
||||
|
||||
import world.phantasmal.psoserv.encryption.BbCipher
|
||||
import world.phantasmal.psoserv.messages.BbMessage
|
||||
import world.phantasmal.psoserv.messages.BbMessageDescriptor
|
||||
|
||||
abstract class BbServer<StateType : ServerState<BbMessage, *, StateType>>(
|
||||
name: String,
|
||||
bindPair: Inet4Pair,
|
||||
) : GameServer<BbMessage, StateType>(name, bindPair) {
|
||||
|
||||
override val messageDescriptor = BbMessageDescriptor
|
||||
|
||||
override fun createCipher() = BbCipher()
|
||||
}
|
@ -5,12 +5,18 @@ import world.phantasmal.psoserv.messages.Message
|
||||
import world.phantasmal.psoserv.messages.MessageDescriptor
|
||||
import java.net.Socket
|
||||
|
||||
abstract class GameServer<MessageType, StateType>(
|
||||
interface ClientReceiver<MessageType : Message> {
|
||||
fun process(message: MessageType): Boolean
|
||||
}
|
||||
|
||||
interface ClientSender<MessageType : Message> {
|
||||
fun send(message: MessageType, encrypt: Boolean = true)
|
||||
}
|
||||
|
||||
abstract class GameServer<MessageType : Message>(
|
||||
name: String,
|
||||
bindPair: Inet4Pair,
|
||||
) : Server(name, bindPair)
|
||||
where MessageType : Message,
|
||||
StateType : ServerState<MessageType, *, StateType> {
|
||||
) : Server(name, bindPair) {
|
||||
|
||||
private var connectionCounter = 0
|
||||
|
||||
@ -18,28 +24,40 @@ abstract class GameServer<MessageType, StateType>(
|
||||
|
||||
override fun clientConnected(clientSocket: Socket) {
|
||||
// Handle each client connection in its own thread.
|
||||
val thread = Thread {
|
||||
val handler = ClientHandler(clientSocket)
|
||||
handler.initializeState()
|
||||
handler.listen()
|
||||
}
|
||||
val thread = Thread { GameClientHandler(clientSocket).listen() }
|
||||
thread.name = "${name}_client_${connectionCounter++}"
|
||||
thread.start()
|
||||
}
|
||||
|
||||
protected abstract fun createCipher(): Cipher
|
||||
|
||||
protected abstract fun initializeState(
|
||||
sender: SocketSender<MessageType>,
|
||||
protected abstract fun createClientReceiver(
|
||||
sender: ClientSender<MessageType>,
|
||||
serverCipher: Cipher,
|
||||
clientCipher: Cipher,
|
||||
): StateType
|
||||
): ClientReceiver<MessageType>
|
||||
|
||||
protected fun unexpectedMessage(message: MessageType): Boolean {
|
||||
logger.debug { "Unexpected message: $message." }
|
||||
return true
|
||||
}
|
||||
|
||||
private inner class GameClientHandler(socket: Socket) :
|
||||
SocketHandler<MessageType>(logger, socket) {
|
||||
|
||||
private inner class ClientHandler(socket: Socket) : SocketHandler<MessageType>(logger, socket) {
|
||||
private val serverCipher = createCipher()
|
||||
private val clientCipher = createCipher()
|
||||
|
||||
private var state: StateType? = null
|
||||
private val handler: ClientReceiver<MessageType> =
|
||||
createClientReceiver(
|
||||
object : ClientSender<MessageType> {
|
||||
override fun send(message: MessageType, encrypt: Boolean) {
|
||||
sendMessage(message, encrypt)
|
||||
}
|
||||
},
|
||||
serverCipher,
|
||||
clientCipher,
|
||||
)
|
||||
|
||||
override val messageDescriptor = this@GameServer.messageDescriptor
|
||||
|
||||
@ -47,22 +65,15 @@ abstract class GameServer<MessageType, StateType>(
|
||||
override val readEncryptCipher: Cipher? = null
|
||||
override val writeEncryptCipher: Cipher = serverCipher
|
||||
|
||||
fun initializeState() {
|
||||
state = initializeState(this, serverCipher, clientCipher)
|
||||
}
|
||||
|
||||
override fun processMessage(message: MessageType): ProcessResult {
|
||||
state = state!!.process(message)
|
||||
|
||||
return if (state is FinalServerState) {
|
||||
override fun processMessage(message: MessageType): ProcessResult =
|
||||
if (handler.process(message)) {
|
||||
ProcessResult.Ok
|
||||
} else {
|
||||
// Give the client some time to disconnect.
|
||||
Thread.sleep(100)
|
||||
|
||||
// Close the connection.
|
||||
ProcessResult.Done
|
||||
} else {
|
||||
ProcessResult.Ok
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,63 @@
|
||||
package world.phantasmal.psoserv.servers
|
||||
|
||||
import world.phantasmal.psoserv.encryption.Cipher
|
||||
import world.phantasmal.psoserv.encryption.PcCipher
|
||||
import world.phantasmal.psoserv.messages.PcMessage
|
||||
import world.phantasmal.psoserv.messages.PcMessageDescriptor
|
||||
|
||||
class PatchServer(
|
||||
name: String,
|
||||
bindPair: Inet4Pair,
|
||||
private val welcomeMessage: String,
|
||||
) : GameServer<PcMessage>(name, bindPair) {
|
||||
|
||||
override val messageDescriptor = PcMessageDescriptor
|
||||
|
||||
override fun createCipher() = PcCipher()
|
||||
|
||||
override fun createClientReceiver(
|
||||
sender: ClientSender<PcMessage>,
|
||||
serverCipher: Cipher,
|
||||
clientCipher: Cipher,
|
||||
): ClientReceiver<PcMessage> = object : ClientReceiver<PcMessage> {
|
||||
init {
|
||||
sender.send(
|
||||
PcMessage.InitEncryption(
|
||||
"Patch Server. Copyright SonicTeam, LTD. 2001",
|
||||
serverCipher.key,
|
||||
clientCipher.key,
|
||||
),
|
||||
encrypt = false,
|
||||
)
|
||||
}
|
||||
|
||||
override fun process(message: PcMessage): Boolean = when (message) {
|
||||
is PcMessage.InitEncryption -> {
|
||||
send(PcMessage.Login())
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
is PcMessage.Login -> {
|
||||
send(PcMessage.WelcomeMessage(welcomeMessage))
|
||||
send(PcMessage.PatchListStart())
|
||||
send(PcMessage.PatchListEnd())
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
is PcMessage.PatchListOk -> {
|
||||
send(PcMessage.PatchDone())
|
||||
|
||||
// Disconnect.
|
||||
false
|
||||
}
|
||||
|
||||
else -> unexpectedMessage(message)
|
||||
}
|
||||
|
||||
private fun send(message: PcMessage) {
|
||||
sender.send(message)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package world.phantasmal.psoserv.servers
|
||||
|
||||
import mu.KLogger
|
||||
import world.phantasmal.psoserv.messages.Message
|
||||
|
||||
abstract class ServerStateContext<MessageType : Message>(
|
||||
val logger: KLogger,
|
||||
private val socketSender: SocketSender<MessageType>,
|
||||
) {
|
||||
fun send(message: MessageType, encrypt: Boolean = true) {
|
||||
socketSender.sendMessage(message, encrypt)
|
||||
}
|
||||
}
|
||||
|
||||
abstract class ServerState<MessageType, ContextType, Self>(
|
||||
protected val ctx: ContextType,
|
||||
) where MessageType : Message,
|
||||
ContextType : ServerStateContext<MessageType>,
|
||||
Self : ServerState<MessageType, ContextType, Self> {
|
||||
|
||||
init {
|
||||
ctx.logger.debug { "Transitioning to ${this::class.simpleName} state." }
|
||||
}
|
||||
|
||||
abstract fun process(message: MessageType): Self
|
||||
|
||||
protected fun unexpectedMessage(message: MessageType): Self {
|
||||
ctx.logger.debug { "Unexpected message: $message." }
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return this as Self
|
||||
}
|
||||
}
|
||||
|
||||
interface FinalServerState
|
@ -15,7 +15,7 @@ import kotlin.math.min
|
||||
abstract class SocketHandler<MessageType : Message>(
|
||||
protected val logger: KLogger,
|
||||
private val socket: Socket,
|
||||
) : SocketSender<MessageType> {
|
||||
) {
|
||||
private val sockName: String = "${socket.remoteSocketAddress}"
|
||||
private val headerSize: Int get() = messageDescriptor.headerSize
|
||||
|
||||
@ -223,7 +223,7 @@ abstract class SocketHandler<MessageType : Message>(
|
||||
socket.close()
|
||||
}
|
||||
|
||||
override fun sendMessage(message: MessageType, encrypt: Boolean) {
|
||||
fun sendMessage(message: MessageType, encrypt: Boolean) {
|
||||
logger.trace {
|
||||
"Sending $message${if (encrypt) "" else " (unencrypted)"}."
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
package world.phantasmal.psoserv.servers
|
||||
|
||||
import world.phantasmal.psoserv.messages.Message
|
||||
|
||||
interface SocketSender<MessageType : Message> {
|
||||
fun sendMessage(message: MessageType, encrypt: Boolean = true)
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package world.phantasmal.psoserv.servers.account
|
||||
|
||||
import world.phantasmal.psoserv.encryption.Cipher
|
||||
import world.phantasmal.psoserv.messages.BbMessage
|
||||
import world.phantasmal.psoserv.servers.BbServer
|
||||
import world.phantasmal.psoserv.servers.Inet4Pair
|
||||
import world.phantasmal.psoserv.servers.SocketSender
|
||||
|
||||
class AccountServer(
|
||||
name: String,
|
||||
bindPair: Inet4Pair,
|
||||
) : BbServer<AccountState>(name, bindPair) {
|
||||
|
||||
override fun initializeState(
|
||||
sender: SocketSender<BbMessage>,
|
||||
serverCipher: Cipher,
|
||||
clientCipher: Cipher,
|
||||
): AccountState {
|
||||
val ctx = AccountContext(logger, sender)
|
||||
|
||||
ctx.send(
|
||||
BbMessage.InitEncryption(
|
||||
"Phantasy Star Online Blue Burst Game Server. Copyright 1999-2004 SONICTEAM.",
|
||||
serverCipher.key,
|
||||
clientCipher.key,
|
||||
),
|
||||
encrypt = false,
|
||||
)
|
||||
|
||||
return AccountState.Authentication(ctx)
|
||||
}
|
||||
}
|
@ -1,250 +0,0 @@
|
||||
package world.phantasmal.psoserv.servers.account
|
||||
|
||||
import mu.KLogger
|
||||
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.messages.*
|
||||
import world.phantasmal.psoserv.servers.FinalServerState
|
||||
import world.phantasmal.psoserv.servers.ServerState
|
||||
import world.phantasmal.psoserv.servers.ServerStateContext
|
||||
import world.phantasmal.psoserv.servers.SocketSender
|
||||
import world.phantasmal.psoserv.utils.crc32Checksum
|
||||
|
||||
class AccountContext(
|
||||
logger: KLogger,
|
||||
socketSender: SocketSender<BbMessage>,
|
||||
var guildCard: Int = -1,
|
||||
var teamId: Int = -1,
|
||||
var slot: Int = 0,
|
||||
var charSelected: Boolean = false,
|
||||
) : ServerStateContext<BbMessage>(logger, socketSender)
|
||||
|
||||
sealed class AccountState(ctx: AccountContext) :
|
||||
ServerState<BbMessage, AccountContext, AccountState>(ctx) {
|
||||
|
||||
class Authentication(ctx: AccountContext) : AccountState(ctx) {
|
||||
override fun process(message: BbMessage): AccountState =
|
||||
if (message is BbMessage.Authenticate) {
|
||||
// TODO: Actual authentication.
|
||||
ctx.guildCard = message.guildCard
|
||||
ctx.teamId = message.teamId
|
||||
ctx.send(
|
||||
BbMessage.AuthData(
|
||||
BbAuthStatus.Success,
|
||||
ctx.guildCard,
|
||||
ctx.teamId,
|
||||
ctx.slot,
|
||||
ctx.charSelected,
|
||||
)
|
||||
)
|
||||
|
||||
Account(ctx)
|
||||
} else {
|
||||
unexpectedMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
class Account(ctx: AccountContext) : AccountState(ctx) {
|
||||
override fun process(message: BbMessage): AccountState =
|
||||
when (message) {
|
||||
is BbMessage.GetAccount -> {
|
||||
// TODO: Send correct guild card number and team ID.
|
||||
ctx.send(BbMessage.Account(0, 0))
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
is BbMessage.CharSelect -> {
|
||||
if (message.select) {
|
||||
// TODO: Verify slot.
|
||||
if (ctx.slot in 0..3) {
|
||||
ctx.slot = message.slot
|
||||
ctx.charSelected = true
|
||||
ctx.send(
|
||||
BbMessage.AuthData(
|
||||
BbAuthStatus.Success,
|
||||
ctx.guildCard,
|
||||
ctx.teamId,
|
||||
ctx.slot,
|
||||
ctx.charSelected,
|
||||
)
|
||||
)
|
||||
ctx.send(
|
||||
BbMessage.CharSelectAck(ctx.slot, BbCharSelectStatus.Select)
|
||||
)
|
||||
} else {
|
||||
ctx.send(
|
||||
BbMessage.CharSelectAck(ctx.slot, BbCharSelectStatus.Nonexistent)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// TODO: Look up character data.
|
||||
ctx.send(
|
||||
BbMessage.CharData(
|
||||
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,
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
is BbMessage.Checksum -> {
|
||||
// TODO: Checksum checking.
|
||||
ctx.send(BbMessage.ChecksumAck(true))
|
||||
|
||||
GuildCardData(ctx)
|
||||
}
|
||||
|
||||
is BbMessage.Disconnect -> Final(ctx)
|
||||
|
||||
else -> unexpectedMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
class GuildCardData(ctx: AccountContext) : AccountState(ctx) {
|
||||
private val guildCardBuffer = Buffer.withSize(54672)
|
||||
|
||||
override fun process(message: BbMessage): AccountState =
|
||||
when (message) {
|
||||
is BbMessage.GetGuildCardHeader -> {
|
||||
ctx.send(
|
||||
BbMessage.GuildCardHeader(
|
||||
guildCardBuffer.size,
|
||||
crc32Checksum(guildCardBuffer),
|
||||
)
|
||||
)
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
is BbMessage.GetGuildCardChunk -> {
|
||||
if (message.cont) {
|
||||
val offset = clamp(
|
||||
message.chunkNo * MAX_CHUNK_SIZE,
|
||||
min = 0,
|
||||
max = guildCardBuffer.size,
|
||||
)
|
||||
val size = (guildCardBuffer.size - offset).coerceAtMost(MAX_CHUNK_SIZE)
|
||||
|
||||
ctx.send(
|
||||
BbMessage.GuildCardChunk(
|
||||
message.chunkNo,
|
||||
guildCardBuffer.cursor(offset, size),
|
||||
)
|
||||
)
|
||||
|
||||
this
|
||||
} else {
|
||||
DownloadFiles(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
else -> unexpectedMessage(message)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_CHUNK_SIZE: Int = 0x6800
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadFiles(ctx: AccountContext) : AccountState(ctx) {
|
||||
private var fileChunkNo = 0
|
||||
|
||||
override fun process(message: BbMessage): AccountState =
|
||||
when (message) {
|
||||
is BbMessage.GetFileList -> {
|
||||
ctx.send(BbMessage.FileList(FILE_LIST))
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
is BbMessage.GetFileChunk -> {
|
||||
val offset = (fileChunkNo * MAX_CHUNK_SIZE).coerceAtMost(FILE_BUFFER.size)
|
||||
val size = (FILE_BUFFER.size - offset).coerceAtMost(MAX_CHUNK_SIZE)
|
||||
|
||||
ctx.send(BbMessage.FileChunk(fileChunkNo, FILE_BUFFER.cursor(offset, size)))
|
||||
|
||||
if (offset + size < FILE_BUFFER.size) {
|
||||
fileChunkNo++
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
is BbMessage.Disconnect -> Final(ctx)
|
||||
|
||||
else -> unexpectedMessage(message)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_CHUNK_SIZE: Int = 0x6800
|
||||
private val FILE_LIST: List<FileListEntry>
|
||||
private val FILE_BUFFER: Buffer
|
||||
|
||||
init {
|
||||
val filenames = listOf(
|
||||
"BattleParamEntry.dat",
|
||||
"BattleParamEntry_ep4.dat",
|
||||
"BattleParamEntry_ep4_on.dat",
|
||||
"BattleParamEntry_lab.dat",
|
||||
"BattleParamEntry_lab_on.dat",
|
||||
"BattleParamEntry_on.dat",
|
||||
"ItemMagEdit.prs",
|
||||
"ItemPMT.prs",
|
||||
"PlyLevelTbl.prs",
|
||||
)
|
||||
val fileBuffers = mutableListOf<Buffer>()
|
||||
|
||||
val fileList = mutableListOf<FileListEntry>()
|
||||
var offset = 0
|
||||
|
||||
for (filename in filenames) {
|
||||
val data = Buffer.fromResource("/world/phantasmal/psoserv/$filename")
|
||||
fileList.add(FileListEntry(data.size, crc32Checksum(data), offset, filename))
|
||||
fileBuffers.add(data)
|
||||
offset += data.size
|
||||
}
|
||||
|
||||
FILE_LIST = fileList
|
||||
|
||||
FILE_BUFFER = Buffer.withSize(offset, Endianness.Little)
|
||||
offset = 0
|
||||
|
||||
for (data in fileBuffers) {
|
||||
data.copyInto(FILE_BUFFER, destinationOffset = offset)
|
||||
offset += data.size
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Final(ctx: AccountContext) : AccountState(ctx), FinalServerState {
|
||||
override fun process(message: BbMessage): AccountState =
|
||||
unexpectedMessage(message)
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
package world.phantasmal.psoserv.servers.auth
|
||||
|
||||
import world.phantasmal.psoserv.encryption.Cipher
|
||||
import world.phantasmal.psoserv.messages.BbMessage
|
||||
import world.phantasmal.psoserv.servers.BbServer
|
||||
import world.phantasmal.psoserv.servers.Inet4Pair
|
||||
import world.phantasmal.psoserv.servers.SocketSender
|
||||
import java.net.Inet4Address
|
||||
|
||||
class AuthServer(
|
||||
name: String,
|
||||
bindPair: Inet4Pair,
|
||||
private val dataServerAddress: Inet4Address,
|
||||
private val dataServerPort: Int,
|
||||
) : BbServer<AuthState>(name, bindPair) {
|
||||
|
||||
override fun initializeState(
|
||||
sender: SocketSender<BbMessage>,
|
||||
serverCipher: Cipher,
|
||||
clientCipher: Cipher,
|
||||
): AuthState {
|
||||
val ctx = AuthContext(logger, sender, dataServerAddress.address, dataServerPort)
|
||||
|
||||
ctx.send(
|
||||
BbMessage.InitEncryption(
|
||||
"Phantasy Star Online Blue Burst Game Server. Copyright 1999-2004 SONICTEAM.",
|
||||
serverCipher.key,
|
||||
clientCipher.key,
|
||||
),
|
||||
encrypt = false,
|
||||
)
|
||||
|
||||
return AuthState.Authentication(ctx)
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
package world.phantasmal.psoserv.servers.auth
|
||||
|
||||
import mu.KLogger
|
||||
import world.phantasmal.psoserv.messages.BbAuthStatus
|
||||
import world.phantasmal.psoserv.messages.BbMessage
|
||||
import world.phantasmal.psoserv.servers.FinalServerState
|
||||
import world.phantasmal.psoserv.servers.ServerState
|
||||
import world.phantasmal.psoserv.servers.ServerStateContext
|
||||
import world.phantasmal.psoserv.servers.SocketSender
|
||||
|
||||
class AuthContext(
|
||||
logger: KLogger,
|
||||
socketSender: SocketSender<BbMessage>,
|
||||
val accountServerAddress: ByteArray,
|
||||
val accountServerPort: Int,
|
||||
) : ServerStateContext<BbMessage>(logger, socketSender)
|
||||
|
||||
sealed class AuthState(ctx: AuthContext) :
|
||||
ServerState<BbMessage, AuthContext, AuthState>(ctx) {
|
||||
|
||||
class Authentication(ctx: AuthContext) : AuthState(ctx) {
|
||||
override fun process(message: BbMessage): AuthState =
|
||||
if (message is BbMessage.Authenticate) {
|
||||
// TODO: Actual authentication.
|
||||
ctx.send(
|
||||
BbMessage.AuthData(
|
||||
BbAuthStatus.Success,
|
||||
message.guildCard,
|
||||
message.teamId,
|
||||
slot = 0,
|
||||
selected = false,
|
||||
)
|
||||
)
|
||||
ctx.send(
|
||||
BbMessage.Redirect(
|
||||
ctx.accountServerAddress,
|
||||
ctx.accountServerPort,
|
||||
)
|
||||
)
|
||||
|
||||
Final(ctx)
|
||||
} else {
|
||||
unexpectedMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
class Final(ctx: AuthContext) : AuthState(ctx), FinalServerState {
|
||||
override fun process(message: BbMessage): AuthState =
|
||||
unexpectedMessage(message)
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
package world.phantasmal.psoserv.servers.patch
|
||||
|
||||
import world.phantasmal.psoserv.encryption.Cipher
|
||||
import world.phantasmal.psoserv.encryption.PcCipher
|
||||
import world.phantasmal.psoserv.messages.PcMessage
|
||||
import world.phantasmal.psoserv.messages.PcMessageDescriptor
|
||||
import world.phantasmal.psoserv.servers.GameServer
|
||||
import world.phantasmal.psoserv.servers.Inet4Pair
|
||||
import world.phantasmal.psoserv.servers.SocketSender
|
||||
|
||||
class PatchServer(
|
||||
name: String,
|
||||
bindPair: Inet4Pair,
|
||||
private val welcomeMessage: String,
|
||||
) : GameServer<PcMessage, PatchState>(name, bindPair) {
|
||||
|
||||
override val messageDescriptor = PcMessageDescriptor
|
||||
|
||||
override fun createCipher() = PcCipher()
|
||||
|
||||
override fun initializeState(
|
||||
sender: SocketSender<PcMessage>,
|
||||
serverCipher: Cipher,
|
||||
clientCipher: Cipher,
|
||||
): PatchState {
|
||||
val ctx = PatchContext(logger, sender, welcomeMessage)
|
||||
|
||||
ctx.send(
|
||||
PcMessage.InitEncryption(
|
||||
"Patch Server. Copyright SonicTeam, LTD. 2001",
|
||||
serverCipher.key,
|
||||
clientCipher.key,
|
||||
),
|
||||
encrypt = false,
|
||||
)
|
||||
|
||||
return PatchState.Welcome(ctx)
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
package world.phantasmal.psoserv.servers.patch
|
||||
|
||||
import mu.KLogger
|
||||
import world.phantasmal.psoserv.messages.PcMessage
|
||||
import world.phantasmal.psoserv.servers.FinalServerState
|
||||
import world.phantasmal.psoserv.servers.ServerState
|
||||
import world.phantasmal.psoserv.servers.ServerStateContext
|
||||
import world.phantasmal.psoserv.servers.SocketSender
|
||||
|
||||
class PatchContext(
|
||||
logger: KLogger,
|
||||
socketSender: SocketSender<PcMessage>,
|
||||
val welcomeMessage: String,
|
||||
) : ServerStateContext<PcMessage>(logger, socketSender)
|
||||
|
||||
sealed class PatchState(ctx: PatchContext) : ServerState<PcMessage, PatchContext, PatchState>(ctx) {
|
||||
class Welcome(ctx: PatchContext) : PatchState(ctx) {
|
||||
override fun process(message: PcMessage): PatchState =
|
||||
if (message is PcMessage.InitEncryption) {
|
||||
ctx.send(PcMessage.Login())
|
||||
|
||||
Login(ctx)
|
||||
} else {
|
||||
unexpectedMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
class Login(ctx: PatchContext) : PatchState(ctx) {
|
||||
override fun process(message: PcMessage): PatchState =
|
||||
if (message is PcMessage.Login) {
|
||||
ctx.send(PcMessage.WelcomeMessage(ctx.welcomeMessage))
|
||||
ctx.send(PcMessage.PatchListStart())
|
||||
ctx.send(PcMessage.PatchListEnd())
|
||||
|
||||
PatchListDone(ctx)
|
||||
} else {
|
||||
unexpectedMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
class PatchListDone(ctx: PatchContext) : PatchState(ctx) {
|
||||
override fun process(message: PcMessage): PatchState =
|
||||
if (message is PcMessage.PatchListOk) {
|
||||
ctx.send(PcMessage.PatchDone())
|
||||
|
||||
Final(ctx)
|
||||
} else {
|
||||
unexpectedMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
class Final(ctx: PatchContext) : PatchState(ctx), FinalServerState {
|
||||
override fun process(message: PcMessage): PatchState =
|
||||
unexpectedMessage(message)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user