Removed independent management server

World server now handles its own management-server-related functionality, a new management server will be written in the future to facilitate cross-world event syncing
World server now runs standalone, this requires the client to be configured to connect the world server port for the world list (world list was previously supplied by the management server on port 5555, which should be swapped to the world server port that is generally 43595 for world 1) work is in progress to update client launchers to make this as seamless as possible
This should resolve issues where players sometimes get a login error claiming they are already logged in
This commit is contained in:
Ceikry
2022-06-09 10:09:57 +00:00
committed by Ryan
parent a61c6072be
commit 2045143378
58 changed files with 15483 additions and 749 deletions
+1 -1
View File
@@ -78,7 +78,7 @@ object Server {
SystemLogger.logErr("Port " + (43594 + GameWorld.settings?.worldId!!) + " is already in use!")
throw e
}
WorldCommunicator.connect()
//WorldCommunicator.connect()
SystemLogger.logInfo(GameWorld.settings?.name + " flags " + GameWorld.settings?.toString())
SystemLogger.logInfo(GameWorld.settings?.name + " started in " + t.duration(false, "") + " milliseconds.")
val scanner = Scanner(System.`in`)
@@ -132,6 +132,7 @@ class WorkForInteractionListener : InteractionListener, LoginListener {
if(entity !is Player) return
val job = getAttribute(entity, "jobs:id", -1)
if (job !in 0 until SlayingJob.values().size) return
val ids = SlayingJob.values()[job].ids
if(event.npc.id in ids){
@@ -170,8 +170,6 @@ class TutorialMagicTutorDialogue(player: Player? = null) : DialoguePlugin(player
"at any time, look for a signpost or use the Lumbridge Home Port Spell."
)
stage = 12
if(WorldCommunicator.isEnabled())
MSPacketRepository.sendInfoUpdate(player)
TutorialStage.removeHintIcon(player)
player.unhook(TutorialKillReceiver)
@@ -16,7 +16,7 @@ interface InteractionListener : ContentInterface{
val GROUNDITEM: Int
get() = 3
fun on(id: Int, type: Int, vararg option: String,handler: (player: Player, node: Node) -> Boolean){
fun on(id: Int, type: Int, vararg option: String, handler: (player: Player, node: Node) -> Boolean){
InteractionListeners.add(id,type,option,handler)
}
fun on(ids: IntArray, type: Int, vararg option: String, handler: (player: Player, node: Node) -> Boolean){
@@ -10,12 +10,13 @@ import core.game.node.entity.skill.Skills
import core.game.system.task.Pulse
import core.game.world.update.flag.context.ChatMessage
import core.game.world.update.flag.player.ChatFlag
import core.net.amsc.MSPacketRepository
import core.net.amsc.WorldCommunicator
import proto.management.ClanMessage
import rs09.game.node.entity.skill.slayer.SlayerManager
import rs09.game.world.GameWorld.Pulser
import rs09.net.packet.`in`.QCPacketType
import rs09.worker.ManagementEvents.publish
import java.nio.ByteBuffer
import java.util.*
/**
@@ -41,11 +42,11 @@ object QCRepository {
if(forClan) {
if (WorldCommunicator.isEnabled()) {
MSPacketRepository.sendClanMessage(player, qcString)
} else {
player?.communication?.clan?.message(player, qcString)
}
val builder = ClanMessage.newBuilder()
builder.sender = player!!.name
builder.clanName = player.communication.clan.owner.lowercase(Locale.getDefault()).replace(" ", "_")
builder.message = qcString
publish(builder.build())
} else {
val ctx = ChatMessage(player, qcString, 0, qcString.length)
ctx.isQuickChat = true
@@ -55,7 +55,6 @@ class LoginParser(val details: PlayerDetails, private val type: LoginType) {
Repository.LOGGED_IN_PLAYERS.remove(player.username)
Repository.lobbyPlayers.remove(player)
Repository.playerNames.remove(player.name)
MSPacketRepository.sendPlayerRemoval(player.name)
flag(AuthResponse.ErrorLoadingProfile)
}
//Repository.getPlayerNames().put(player.getName(), player);
@@ -63,16 +62,6 @@ class LoginParser(val details: PlayerDetails, private val type: LoginType) {
override fun pulse(): Boolean {
try {
if (details.session.isActive) {
val p = Repository.getPlayerByName(player.name)
if (p != null) {
p.clear()
Repository.playerNames.remove(p.name)
Repository.lobbyPlayers.remove(p)
Repository.removePlayer(p)
}
if (!Repository.players.contains(player)) {
Repository.addPlayer(player)
}
loginListeners.forEach(Consumer { listener: LoginListener -> listener.login(player) }) //Run our login hooks
parser.runContentHooks() //Run our saved-content-parsing hooks
player.details.session.setObject(player)
@@ -83,7 +72,6 @@ class LoginParser(val details: PlayerDetails, private val type: LoginType) {
player.monitor.log(player.details.macAddress, PlayerMonitor.ADDRESS_LOG)
} else {
Repository.playerNames.remove(player.name)
MSPacketRepository.sendPlayerRemoval(player.name)
}
} catch (t: Throwable) {
t.printStackTrace()
@@ -91,7 +79,6 @@ class LoginParser(val details: PlayerDetails, private val type: LoginType) {
Repository.LOGGED_IN_PLAYERS.remove(player.username)
Repository.lobbyPlayers.remove(player)
Repository.playerNames.remove(player.name)
MSPacketRepository.sendPlayerRemoval(player.name)
flag(AuthResponse.ErrorLoadingProfile)
}
return true
@@ -112,15 +99,9 @@ class LoginParser(val details: PlayerDetails, private val type: LoginType) {
player.updateSceneGraph(true)
player.configManager.init()
LoginConfiguration.configureGameWorld(player)
Repository.playerNames[player.name] = player
GameWorld.Pulser.submit(object : Pulse(1) {
override fun pulse(): Boolean {
if (!Repository.players.contains(player)) {
Repository.addPlayer(player)
}
return true
}
})
if (!Repository.players.contains(player)) {
Repository.addPlayer(player)
}
}
/**
@@ -128,18 +109,12 @@ class LoginParser(val details: PlayerDetails, private val type: LoginType) {
* @return `True` if the request is valid.
*/
private fun validateRequest(): Boolean {
if (WorldCommunicator.getState() == ManagementServerState.CONNECTING) {
return flag(AuthResponse.LoginServerOffline)
}
if (!details.session.isActive) {
return false
}
if (SystemManager.isUpdating()) {
return flag(AuthResponse.Updating)
}
if (Repository.getPlayerByName(details.username).also { gamePlayer = it } != null && gamePlayer!!.session.isActive) {
return flag(AuthResponse.AlreadyOnline)
}
return if (details.isBanned) {
flag(AuthResponse.AccountDisabled)
} else true
@@ -114,4 +114,8 @@ object SystemLogger {
{
SystemLogger.logInfo("[SHUTDOWN] $message")
}
fun logMS(s: String) {
if(s.isNotBlank()) t.println("${getTime()}: ${TextColors.gray("[ MS] $s")}")
}
}
@@ -135,12 +135,14 @@ object Repository {
fun addPlayer(player: Player){
players.add(player)
uid_map[player.details.usernameHashcode] = player
playerNames[player.name] = player
}
@JvmStatic
fun removePlayer(player: Player){
players.remove(player)
uid_map.remove(player.details.usernameHashcode)
playerNames.remove(player.name)
}
/**
@@ -6,6 +6,7 @@ import core.game.system.communication.CommunicationInfo
import core.net.IoReadEvent
import core.net.IoSession
import rs09.auth.AuthResponse
import rs09.game.system.SystemLogger
import rs09.game.world.GameWorld
import rs09.net.packet.`in`.Login
import java.nio.ByteBuffer
@@ -17,25 +18,21 @@ import java.nio.ByteBuffer
class LoginReadEvent(session: IoSession?, buffer: ByteBuffer?) : IoReadEvent(session, buffer) {
override fun read(session: IoSession, buffer: ByteBuffer) {
val (response, info) = Login.decodeFromBuffer(buffer)
if(response != AuthResponse.Success || info == null) {
session.write(response)
return
}
val (authResponse, accountInfo) = GameWorld.authenticator.checkLogin(info.username, info.password)
if(authResponse != AuthResponse.Success || accountInfo == null) {
session.write(authResponse)
return
}
val details = PlayerDetails(info.username)
details.accountInfo = accountInfo
details.communication.parse(accountInfo)
session.clientInfo = ClientInfo(info.displayMode, info.windowMode, info.screenWidth, info.screenHeight)
session.isaacPair = info.isaacPair
Login.proceedWith(session, details, info.opcode)
}
}
@@ -3,18 +3,24 @@ package rs09.net.packet.`in`
import core.cache.crypto.ISAACCipher
import core.cache.crypto.ISAACPair
import core.cache.misc.buffer.ByteBufferUtils
import core.game.node.entity.player.Player
import core.game.node.entity.player.info.PlayerDetails
import core.game.node.entity.player.info.UIDInfo
import core.game.node.entity.player.info.login.LoginType
import core.net.Constants
import core.net.IoSession
import core.net.amsc.WorldCommunicator
import core.tools.StringUtils
import proto.management.JoinClanRequest
import proto.management.PlayerStatusUpdate
import proto.management.RequestContactInfo
import rs09.ServerConstants
import rs09.auth.AuthResponse
import rs09.game.node.entity.player.info.login.LoginParser
import rs09.game.system.SystemLogger
import rs09.game.world.GameWorld
import rs09.game.world.repository.Repository
import rs09.worker.ManagementEvents
import rs09.worker.ManagementEvents.publish
import java.math.BigInteger
import java.nio.BufferUnderflowException
import java.nio.ByteBuffer
@@ -68,6 +74,11 @@ object Login {
info.isaacPair = produceISAACPairFrom(decryptedBuffer)
info.username = StringUtils.longToString(decryptedBuffer.long)
info.password = ByteBufferUtils.getString(decryptedBuffer)
if (Repository.getPlayerByName(info.username) != null) {
return Pair(AuthResponse.AlreadyOnline, info)
}
return Pair(AuthResponse.Success, info)
} catch (e: Exception) {
SystemLogger.logErr("Exception encountered during login packet parsing! See stack trace below.")
@@ -109,6 +120,31 @@ object Login {
Repository.LOGGED_IN_PLAYERS.add(details.username)
details.session = session
details.info.translate(UIDInfo(details.ipAddress, "DEPRECATED", "DEPRECATED", "DEPRECATED"))
WorldCommunicator.register(LoginParser(details, LoginType.fromType(opcode)))
val player = Player(details)
if (!Repository.players.contains(player)) {
Repository.addPlayer(player)
}
LoginParser(details, LoginType.fromType(opcode)).initialize(player, opcode == RECONNECT_LOGIN_OP)
sendMSEvents(details)
}
private fun sendMSEvents(details: PlayerDetails) {
val statusEvent = PlayerStatusUpdate.newBuilder()
statusEvent.username = details.username
statusEvent.world = GameWorld.settings!!.worldId
statusEvent.notifyFriendsOnly = false
publish(statusEvent.build())
val contactEvent = RequestContactInfo.newBuilder()
contactEvent.username = details.username
contactEvent.world = GameWorld.settings!!.worldId
publish(contactEvent.build())
if (!details.communication.currentClan.isNullOrEmpty() && details.communication.clan == null) {
val clanEvent = JoinClanRequest.newBuilder()
clanEvent.username = details.username
clanEvent.clanName = details.communication.currentClan
publish(clanEvent.build())
}
}
}
@@ -8,4 +8,5 @@ interface AccountStorageProvider {
fun store(info: UserAccountInfo)
fun update(info: UserAccountInfo)
fun remove(info: UserAccountInfo)
fun getOnlineFriends(username: String) : List<String>
}
@@ -24,4 +24,8 @@ class InMemoryStorageProvider : AccountStorageProvider {
override fun remove(info: UserAccountInfo) {
storage.remove(info.username)
}
override fun getOnlineFriends(username: String): List<String> {
return ArrayList()
}
}
@@ -1,7 +1,9 @@
package rs09.storage
import core.game.system.communication.CommunicationInfo
import rs09.auth.UserAccountInfo
import rs09.game.system.SystemLogger
import rs09.game.world.repository.Repository
import java.lang.Long.max
import java.sql.*
@@ -155,6 +157,24 @@ class SQLStorageProvider : AccountStorageProvider {
}
}
override fun getOnlineFriends(username: String): List<String> {
val friends = ArrayList<String>()
var fTokens = ""
val conn = getConnection()
conn.use {
val friendsQuery = it.prepareStatement(GET_ALL_FRIENDS_QUERY)
friendsQuery.setString(1, username)
val f = friendsQuery.executeQuery()
if (f.next()) {
fTokens = f.getString(1)
}
}
val contacts = CommunicationInfo.parseContacts(fTokens)
for ((friendName, _) in contacts) if (Repository.getPlayerByName(friendName) != null) friends.add(friendName)
return friends
}
companion object {
private const val usernameQuery = "SELECT username FROM members WHERE username = ?;"
private const val removeInfoQuery = "DELETE FROM members WHERE username = ?;"
@@ -227,5 +247,7 @@ class SQLStorageProvider : AccountStorageProvider {
15 to "lastLogin",
16 to "online"
)
private val GET_ALL_FRIENDS_QUERY = "SELECT contacts FROM players WHERE username = ?;"
}
}
@@ -0,0 +1,336 @@
package rs09.worker
import api.sendMessage
import com.google.protobuf.Message
import core.game.node.entity.player.Player
import core.game.system.communication.ClanEntry
import core.game.system.communication.ClanRank
import core.game.system.communication.ClanRepository
import core.game.system.communication.CommunicationInfo
import core.net.packet.PacketRepository
import core.net.packet.context.ContactContext
import core.net.packet.context.MessageContext
import core.net.packet.out.CommunicationMessage
import core.net.packet.out.ContactPackets
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import proto.management.ClanJoinNotification
import proto.management.ClanLeaveNotification
import proto.management.ClanMessage
import proto.management.FriendUpdate
import proto.management.JoinClanRequest
import proto.management.LeaveClanRequest
import proto.management.PlayerStatusUpdate
import proto.management.PrivateMessage
import proto.management.RequestClanInfo
import proto.management.RequestContactInfo
import proto.management.SendClanInfo
import proto.management.SendClanInfo.ClanMember
import proto.management.SendContactInfo
import proto.management.SendContactInfo.Contact
import rs09.game.system.SystemLogger
import rs09.game.world.GameWorld
import rs09.game.world.repository.Repository
import java.util.Deque
import java.util.LinkedList
import java.util.concurrent.BlockingDeque
import java.util.concurrent.LinkedBlockingDeque
import kotlin.reflect.jvm.internal.impl.descriptors.Visibilities.Private
/**
* Processes management-related events e.g clan messages, etc.
*/
object ManagementEvents {
private var isRunning: Boolean = true
private val eventQueue: BlockingDeque<Message> = LinkedBlockingDeque()
private val waitingOnClanInfo = HashMap<String, Deque<Message>>()
private val hasRequestedClanInfo = HashMap<String, Boolean>()
val job = GlobalScope.launch {
while (isRunning) {
val event = withContext(Dispatchers.IO) { eventQueue.take() }
try {
handleEvent(event)
handleLoggingFor(event)
} catch (ignored: Exception) {}
}
}
private fun handleLoggingFor(event: Message) {
when (event) {
is PlayerStatusUpdate -> SystemLogger.logMS("${event.username} -(WLD)> ${event.world}")
is RequestContactInfo -> SystemLogger.logMS("${event.username} -> RQ CONTACT INFO")
is SendContactInfo -> SystemLogger.logMS("${event.username} <- SND CONTACT INFO")
is PrivateMessage -> SystemLogger.logMS("[PM] ${event.sender}->${event.receiver}: ${event.message}")
is ClanMessage -> SystemLogger.logMS("[CM:${event.clanName}] ${event.sender}: ${event.message}")
is JoinClanRequest -> SystemLogger.logMS("${event.username} +CL ${event.clanName}")
is LeaveClanRequest -> SystemLogger.logMS("${event.username} -CL ${event.clanName}")
is RequestClanInfo -> SystemLogger.logMS("REQUEST CLAN INFO: ${event.clanOwner}")
is SendClanInfo -> SystemLogger.logMS("RECEIVE CLAN INFO: ${event.clanOwner}->${event.clanName}")
is ClanJoinNotification -> SystemLogger.logMS("${event.username} JOINED CLAN ${event.clanName}")
is ClanLeaveNotification -> SystemLogger.logMS("${event.username} LEFT CLAN ${event.clanName}")
}
}
@JvmStatic fun publish(event: Message) {
eventQueue.offer(event)
}
private fun handleEvent(event: Message) {
when (event) {
is PlayerStatusUpdate -> {
val notifiablePlayers = if (event.notifyFriendsOnly) {
GameWorld.accountStorage.getOnlineFriends(event.username)
} else {
Repository.playerNames.keys.toList()
}.filter { Repository.getPlayerByName(it)?.communication?.contacts?.containsKey(event.username) == true }
for (playerName in notifiablePlayers) {
val p = Repository.getPlayerByName(playerName) ?: continue
p.communication.contacts[event.username]?.worldId = event.world
PacketRepository.send(ContactPackets::class.java, ContactContext(p, event.username, event.world))
}
}
is RequestContactInfo -> {
val response = SendContactInfo.newBuilder()
response.username = event.username
val info = GameWorld.accountStorage.getAccountInfo(event.username)
val contacts = CommunicationInfo.parseContacts(info.contacts)
for ((username, contact) in contacts) {
val online = Repository.getPlayerByName(username) != null
val cbuild = Contact.newBuilder()
cbuild.username = username
cbuild.world = if (online) GameWorld.settings!!.worldId else 0
cbuild.rank = contact.rank.ordinal
response.addContacts(cbuild)
}
val blocked = info.blocked.split(",")
for (user in blocked) response.addBlocked(user)
publish(response.build())
}
is SendContactInfo -> {
val p = Repository.getPlayerByName(event.username) ?: return
PacketRepository.send(
ContactPackets::class.java,
ContactContext(p, ContactContext.UPDATE_STATE_TYPE)
)
p.communication.contacts.clear()
p.communication.blocked.clear()
for (contact in event.contactsList) {
val c = core.game.system.communication.Contact(contact.username)
p.communication.contacts[contact.username] = c
c.worldId = contact.world
c.rank = ClanRank.values()[contact.rank]
PacketRepository.send(
ContactPackets::class.java,
ContactContext(p, contact.username, contact.world)
)
}
for (blocked in event.blockedList) {
p.communication.blocked.add(blocked)
}
PacketRepository.send(
ContactPackets::class.java,
ContactContext(p, ContactContext.IGNORE_LIST_TYPE)
)
}
is PrivateMessage -> {
val sender = Repository.getPlayerByName(event.sender)
val receiver = Repository.getPlayerByName(event.receiver)
if (sender != null) {
PacketRepository.send(
CommunicationMessage::class.java,
MessageContext(sender, event.receiver, event.rank, MessageContext.SEND_MESSAGE, event.message)
)
}
if (receiver != null) {
PacketRepository.send(
CommunicationMessage::class.java,
MessageContext(receiver, event.sender, event.rank, MessageContext.RECIEVE_MESSAGE, event.message)
)
}
}
is JoinClanRequest -> {
val p = Repository.getPlayerByName(event.username) ?: return
if (shouldWaitForClanInfo(event.clanName)) {
queueUntilClanInfo(event.clanName, event)
return
}
val clan = ClanRepository.get(event.clanName)
if (clan == null) {
sendMessage(p, "The channel you tried to join does not exist.:clan:")
} else {
clan.enter(p)
p.communication.clan = clan
}
}
is ClanJoinNotification -> {
if (event.world == GameWorld.settings!!.worldId) return
if (shouldWaitForClanInfo(event.clanName)) {
queueUntilClanInfo(event.clanName, event)
return
}
val clan = ClanRepository.get(event.clanName)
val entry = ClanEntry(event.username, event.world)
clan.players.add(entry)
clan.update()
}
is LeaveClanRequest -> {
val p = Repository.getPlayerByName(event.username) ?: return
if (shouldWaitForClanInfo(event.clanName)) {
queueUntilClanInfo(event.clanName, event)
return
}
val clan = ClanRepository.get(event.clanName)
if (clan == null) {
sendMessage(p, "Error leaving clan. Please relog.")
} else {
clan.leave(p, true)
}
}
is ClanLeaveNotification -> {
if (shouldWaitForClanInfo(event.clanName)) {
queueUntilClanInfo(event.clanName, event)
return
}
val clan = ClanRepository.get(event.clanName)
val entry = clan.players.firstOrNull { it.name.equals(event.username) } ?: return
clan.players.remove(entry)
clan.update()
}
is RequestClanInfo -> {
val clan = ClanRepository.get(event.clanOwner)
val response = SendClanInfo.newBuilder()
if (clan == null) {
response.hasInfo = false
response.clanOwner = event.clanOwner
} else {
response.hasInfo = true
response.clanName = clan.name
response.clanOwner = event.clanOwner
for (member in clan.players) {
val cmBuilder = ClanMember.newBuilder()
cmBuilder.username = member.name
cmBuilder.world = member.worldId
cmBuilder.rank = (clan.ranks[member.name] ?: ClanRank.NONE).ordinal
response.addMembers(cmBuilder)
}
}
publish(response.build())
}
is SendClanInfo -> {
if (event.hasInfo) {
val clan = ClanRepository.getClans().getOrPut(event.clanOwner) { ClanRepository(event.clanOwner) }
clan.name = event.clanName
clan.joinRequirement = ClanRank.values()[event.joinRequirement]
clan.kickRequirement = ClanRank.values()[event.kickRequirement]
clan.messageRequirement = ClanRank.values()[event.messageRequirement]
clan.lootRequirement = ClanRank.values()[event.lootRequirement]
for (member in event.membersList) {
val entry = ClanEntry(member.username, member.world)
clan.ranks[member.username] = ClanRank.values()[member.rank]
if (member.world == GameWorld.settings!!.worldId) {
val p = Repository.getPlayerByName(member.username)
entry.player = p
p?.communication?.clan = clan
}
clan.players.add(entry)
}
clan.update()
} else {
val info = GameWorld.accountStorage.getAccountInfo(event.clanOwner)
if (info.clanName.isNotEmpty()) {
val reqs = CommunicationInfo.parseClanRequirements(info.clanReqs)
val c = ClanRepository(event.clanOwner)
val contacts = CommunicationInfo.parseContacts(info.contacts)
c.name = info.clanName
c.joinRequirement = reqs[0]
c.messageRequirement = reqs[1]
c.kickRequirement = reqs[2]
c.lootRequirement = reqs[3]
for ((username, contact) in contacts) {
c.ranks[username] = contact.rank
}
ClanRepository.getClans()[event.clanOwner] = c
}
}
val queuedEvents = waitingOnClanInfo[event.clanOwner] ?: return
while (queuedEvents.peek() != null) {
publish(queuedEvents.pop())
}
}
is ClanMessage -> {
if (shouldWaitForClanInfo(event.clanName)) {
queueUntilClanInfo(event.clanName, event)
return
}
val clan = ClanRepository.get(event.clanName)
for (member in clan.players.filter { it.player != null }) {
PacketRepository.send(
CommunicationMessage::class.java,
MessageContext(member.player, event.sender, event.rank, MessageContext.CLAN_MESSAGE, event.message)
)
}
}
}
}
private fun queueUntilClanInfo(clanName: String, message: Message) {
val queue = waitingOnClanInfo.getOrPut(clanName) {LinkedList()}
queue.offer(message)
if (hasRequestedClanInfo[clanName] == null) {
val request = RequestClanInfo.newBuilder()
request.clanOwner = clanName
request.world = GameWorld.settings!!.worldId
publish(request.build())
hasRequestedClanInfo[clanName] = true
}
}
private fun shouldWaitForClanInfo(clanName: String): Boolean {
return ClanRepository.get(clanName) == null && hasRequestedClanInfo[clanName] == null
}
}