diff --git a/Server/src/main/java/core/game/system/communication/CommunicationInfo.java b/Server/src/main/java/core/game/system/communication/CommunicationInfo.java index 2dee6f8ae..99d33c794 100644 --- a/Server/src/main/java/core/game/system/communication/CommunicationInfo.java +++ b/Server/src/main/java/core/game/system/communication/CommunicationInfo.java @@ -2,6 +2,8 @@ package core.game.system.communication; import core.cache.misc.buffer.ByteBufferUtils; import core.game.node.entity.player.Player; +import org.jetbrains.annotations.NotNull; +import rs09.auth.UserAccountInfo; import rs09.game.system.SystemLogger; import core.game.system.monitor.PlayerMonitor; import core.game.system.mysql.SQLTable; @@ -18,10 +20,7 @@ import core.net.packet.out.ContactPackets; import core.tools.StringUtils; import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; /** @@ -153,7 +152,7 @@ public final class CommunicationInfo { } } clanName = ((String) table.getColumn("clanName").getValue()); - currentClan = (String) table.getColumn("currentClan").getValue(); + currentClan = (String) table.getColumn("currentClan").getValue(); String clanReqs = (String) table.getColumn("clanReqs").getValue(); if (clanReqs == "") { return; @@ -168,21 +167,21 @@ public final class CommunicationInfo { } rank = ClanRank.values()[ordinal]; switch (i) { - case 0: - joinRequirement = rank; - break; - case 1: - messageRequirement = rank; - break; - case 2: - if (ordinal < 3 || ordinal > 8) { + case 0: + joinRequirement = rank; + break; + case 1: + messageRequirement = rank; + break; + case 2: + if (ordinal < 3 || ordinal > 8) { + break; + } + kickRequirement = rank; + break; + case 3: + lootRequirement = rank; break; - } - kickRequirement = rank; - break; - case 3: - lootRequirement = rank; - break; } } } @@ -478,13 +477,13 @@ public final class CommunicationInfo { return false; } switch (target.getSettings().getPrivateChatSetting()) { - case 1: - if (!hasContact(target, player.getName())) { + case 1: + if (!hasContact(target, player.getName())) { + return false; + } + return true; + case 2: return false; - } - return true; - case 2: - return false; } return true; } @@ -634,4 +633,54 @@ public final class CommunicationInfo { public void setLootShare(boolean lootShare) { this.lootShare = lootShare; } + + public void parse(@NotNull UserAccountInfo accountInfo) { + blocked.addAll(Arrays.asList(accountInfo.getBlocked().split(","))); + clanName = accountInfo.getClanName(); + currentClan = accountInfo.getCurrentClan(); + + String clanReqs = accountInfo.getClanReqs(); + String[] tokens = clanReqs.split(","); + ClanRank rank = null; + int ordinal = 0; + for (int i = 0; i < tokens.length; i++) { + ordinal = Integer.parseInt(tokens[i]); + if (ordinal < 0 || ordinal > ClanRank.values().length -1) { + continue; + } + rank = ClanRank.values()[ordinal]; + switch (i) { + case 0: + joinRequirement = rank; + break; + case 1: + messageRequirement = rank; + break; + case 2: + if (ordinal < 3 || ordinal > 8) { + break; + } + kickRequirement = rank; + break; + case 3: + lootRequirement = rank; + break; + } + } + + String contacts = accountInfo.getContacts(); + if (!contacts.isEmpty()) { + String[] datas = contacts.split("~"); + Contact contact = null; + for (String d : datas) { + tokens = d.replace("{", "").replace("}", "").split(","); + if (tokens.length == 0) { + continue; + } + contact = new Contact(tokens[0]); + contact.setRank(ClanRank.values()[Integer.parseInt(tokens[1])]); + this.contacts.put(tokens[0], contact); + } + } + } } \ No newline at end of file diff --git a/Server/src/main/java/core/net/amsc/MSPacketRepository.java b/Server/src/main/java/core/net/amsc/MSPacketRepository.java index 8e7ca3a4f..eab1c539f 100644 --- a/Server/src/main/java/core/net/amsc/MSPacketRepository.java +++ b/Server/src/main/java/core/net/amsc/MSPacketRepository.java @@ -312,11 +312,11 @@ public final class MSPacketRepository { switch (response) { case AlreadyOnline: player = Repository.getPlayerByName(username); - if (player == null || player.getSession().isActive() || !player.getSession().getAddress().equals(details.getSession().getAddress())) { +/* if (player == null || player.getSession().isActive() || !player.getSession().getAddress().equals(details.getSession().getAddress())) { details.getSession().write(response, true); break; - } - player.getPacketDispatch().sendLogout(); + }*/ + if (player != null) player.getPacketDispatch().sendLogout(); case Success: if (!details.getSession().isActive()) { sendPlayerRemoval(username); diff --git a/Server/src/main/kotlin/rs09/auth/UserAccountInfo.kt b/Server/src/main/kotlin/rs09/auth/UserAccountInfo.kt index f16637b79..d73318609 100644 --- a/Server/src/main/kotlin/rs09/auth/UserAccountInfo.kt +++ b/Server/src/main/kotlin/rs09/auth/UserAccountInfo.kt @@ -21,10 +21,31 @@ class UserAccountInfo( ) { companion object { @JvmStatic fun createDefault() : UserAccountInfo { - return UserAccountInfo("", "", 0, 0, 0, "", "", 0L, 0L, "", "", "", "", "1,0,8,9", 0L, 0L, false) + return UserAccountInfo("", "", 0, 0, 0, "", "", 0L, 0L, "", "", "", "", "1,0,8,9", 0L, 0L, false).also { it.setInitialReferenceValues() } } } + lateinit var initialValues: Array + + fun setInitialReferenceValues() { + initialValues = toArray() + } + + fun getChangedFields(): Pair, Array> { + val current = toArray() + val changed = ArrayList() + + for(i in current.indices) { + if (current[i] != initialValues[i]) changed.add(i) + } + + return Pair(changed, current) + } + + fun toArray(): Array { + return arrayOf(username, password, uid, rights, credits, ip, lastUsedIp, muteEndTime, banEndTime, contacts, blocked, clanName, currentClan, clanReqs, timePlayed, lastLogin, online) + } + override fun toString(): String { return "USER:$username,PASS:$password,UID:$uid,RIGHTS:$rights,CREDITS:$credits,IP:$ip,LASTIP:$lastUsedIp" } diff --git a/Server/src/main/kotlin/rs09/game/node/entity/player/info/login/LoginParser.kt b/Server/src/main/kotlin/rs09/game/node/entity/player/info/login/LoginParser.kt index f0df87236..be04ff919 100644 --- a/Server/src/main/kotlin/rs09/game/node/entity/player/info/login/LoginParser.kt +++ b/Server/src/main/kotlin/rs09/game/node/entity/player/info/login/LoginParser.kt @@ -14,86 +14,23 @@ import core.net.amsc.MSPacketRepository import core.net.amsc.ManagementServerState import core.net.amsc.WorldCommunicator import rs09.auth.AuthResponse -import rs09.game.system.SystemLogger import rs09.game.world.GameWorld import rs09.game.world.GameWorld.loginListeners import rs09.game.world.repository.Repository -import java.util.concurrent.TimeUnit import java.util.concurrent.locks.Lock import java.util.concurrent.locks.ReentrantLock import java.util.function.Consumer /** * Parses the login of a player. - * @author Emperor - * @author Vexia */ -class LoginParser( - /** - * The player details file. - */ - val details: PlayerDetails, - /** - * The login type. - */ - private val type: LoginType -) : Runnable { - /** - * Gets the player details. - * @return The player details. - */ - +class LoginParser(val details: PlayerDetails, private val type: LoginType) { /** * The player in the game, used for reconnect login type. */ private var gamePlayer: Player? = null - /** - * Gets the timeStamp. - * @return the timeStamp - */ - /** - * The time stamp. - */ - val timeStamp: Int - override fun run() { - try { - LOCK.tryLock(1000L, TimeUnit.MILLISECONDS) - } catch (e: Exception) { - println(e) - LOCK.unlock() - return - } - try { - if (validateRequest()) { - handleLogin() - } - } catch (t: Throwable) { - t.printStackTrace() - try { - flag(AuthResponse.ErrorLoadingProfile) - Repository.LOGGED_IN_PLAYERS.remove(details.username) - } catch (e: Throwable) { - e.printStackTrace() - } - } - LOCK.unlock() - } - - /** - * Handles the actual login. - */ - private fun handleLogin() { - val p = worldInstance - val player = p ?: Player(details) - player.setAttribute("login_type", type) - if (p != null) { // Reconnecting - p.updateDetails(details) - reconnect(p, type) - return - } - initialize(player, false) - } + val timeStamp: Int = GameWorld.ticks /** * Initializes the player. @@ -101,8 +38,9 @@ class LoginParser( * @param reconnect If the player data should be parsed. */ fun initialize(player: Player, reconnect: Boolean) { + if(!validateRequest()) return if (reconnect) { - reconnect(player, type) + reconnect(player) return } try { @@ -160,25 +98,12 @@ class LoginParser( }) } - /** - * Gets the player instance in the current world. - * @return The player instance, if found. - */ - private val worldInstance: Player? - private get() { - var player = Repository.disconnectionQueue[details.username] - if (player == null) { - player = gamePlayer - } - return player - } - /** * Initializes a reconnecting player. * @param player The player. * @param type The login type. */ - private fun reconnect(player: Player, type: LoginType) { + private fun reconnect(player: Player) { Repository.disconnectionQueue.remove(details.username) player.initReconnect() player.isActive = true @@ -202,11 +127,6 @@ class LoginParser( * @return `True` if the request is valid. */ private fun validateRequest(): Boolean { - //This is supposed to prevent the double-logging issue. Will it work? Who knows. - if (Repository.LOGGED_IN_PLAYERS.contains(details.username)) { - SystemLogger.logWarn("LOGGED_IN_PLAYERS contains ${details.username}") - return flag(AuthResponse.AlreadyOnline) - } if (WorldCommunicator.getState() == ManagementServerState.CONNECTING) { return flag(AuthResponse.LoginServerOffline) } @@ -233,20 +153,4 @@ class LoginParser( details.session.write(response, true) return response == AuthResponse.Success } - - companion object { - /** - * The lock used to disable 2 of the same player being logged in. - */ - private val LOCK: Lock = ReentrantLock() - } - - /** - * Constructs a new `LoginParser` `Object`. - * @param details the player details. - * @param type The login type. - */ - init { - timeStamp = GameWorld.ticks - } } \ No newline at end of file diff --git a/Server/src/main/kotlin/rs09/net/event/LoginReadEvent.kt b/Server/src/main/kotlin/rs09/net/event/LoginReadEvent.kt index fca310685..68f256f12 100644 --- a/Server/src/main/kotlin/rs09/net/event/LoginReadEvent.kt +++ b/Server/src/main/kotlin/rs09/net/event/LoginReadEvent.kt @@ -2,6 +2,7 @@ package rs09.net.event import core.game.node.entity.player.info.ClientInfo import core.game.node.entity.player.info.PlayerDetails +import core.game.system.communication.CommunicationInfo import core.net.IoReadEvent import core.net.IoSession import rs09.auth.AuthResponse @@ -31,6 +32,7 @@ class LoginReadEvent(session: IoSession?, buffer: ByteBuffer?) : IoReadEvent(ses 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 diff --git a/Server/src/main/kotlin/rs09/storage/SQLStorageProvider.kt b/Server/src/main/kotlin/rs09/storage/SQLStorageProvider.kt index d27a96b6c..67b85fa82 100644 --- a/Server/src/main/kotlin/rs09/storage/SQLStorageProvider.kt +++ b/Server/src/main/kotlin/rs09/storage/SQLStorageProvider.kt @@ -1,6 +1,7 @@ package rs09.storage import rs09.auth.UserAccountInfo +import rs09.game.system.SystemLogger import java.lang.Long.max import java.sql.* @@ -59,6 +60,7 @@ class SQLStorageProvider : AccountStorageProvider { result.getLong(16) .let { userData.lastLogin = max(0L, it) } result.getBoolean(17).let { userData.online = it } + userData.setInitialReferenceValues() return userData } else { return UserAccountInfo.createDefault() @@ -103,13 +105,17 @@ class SQLStorageProvider : AccountStorageProvider { if (result.next()) { info.uid = result.getInt(1) } + info.setInitialReferenceValues() } } override fun update(info: UserAccountInfo) { + val (updatedFields, rawData) = info.getChangedFields() + val updateQueryString = buildUpdateInfoQuery(updatedFields) val conn = getConnection() + conn.use { - val compiledUpdateInfoQuery = it.prepareStatement(updateInfoQuery) + val compiledUpdateInfoQuery = it.prepareStatement(updateQueryString) val emptyInfo = UserAccountInfo.createDefault() if (info == emptyInfo) { throw IllegalStateException("Tried to store empty data!") @@ -122,24 +128,21 @@ class SQLStorageProvider : AccountStorageProvider { if (info == emptyInfo) { throw IllegalStateException("Tried to store empty data!") } + if (updatedFields.isEmpty()) return - compiledUpdateInfoQuery.setString(1, info.username) - compiledUpdateInfoQuery.setString(2, info.password) - compiledUpdateInfoQuery.setInt(3, info.rights) - compiledUpdateInfoQuery.setInt(4, info.credits) - compiledUpdateInfoQuery.setString(5, info.ip) - compiledUpdateInfoQuery.setLong(6, info.muteEndTime) - compiledUpdateInfoQuery.setLong(7, info.banEndTime) - compiledUpdateInfoQuery.setString(8, info.contacts) - compiledUpdateInfoQuery.setString(9, info.blocked) - compiledUpdateInfoQuery.setString(10, info.clanName) - compiledUpdateInfoQuery.setString(11, info.currentClan) - compiledUpdateInfoQuery.setString(12, info.clanReqs) - compiledUpdateInfoQuery.setLong(13, info.timePlayed) - compiledUpdateInfoQuery.setLong(14, info.lastLogin) - compiledUpdateInfoQuery.setBoolean(15, info.online) - compiledUpdateInfoQuery.setInt(16, info.uid) + var fieldIndex = 1 + for (updatedFieldIndex in updatedFields) { + when(val data = rawData[updatedFieldIndex]) { + is String -> compiledUpdateInfoQuery.setString(fieldIndex++, data) + is Int -> compiledUpdateInfoQuery.setInt(fieldIndex++, data) + is Boolean -> compiledUpdateInfoQuery.setBoolean(fieldIndex++, data) + is Long -> compiledUpdateInfoQuery.setLong(fieldIndex++, data) + } + } + compiledUpdateInfoQuery.setInt(fieldIndex, info.uid) compiledUpdateInfoQuery.execute() + + info.initialValues = rawData } } @@ -193,22 +196,36 @@ class SQLStorageProvider : AccountStorageProvider { "online" + ") VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);" - private const val updateInfoQuery = "UPDATE members SET " + - "username = ?," + - "password = ?," + - "rights = ?," + - "credits = ?," + - "lastGameIp = ?," + - "muteTime = ?," + - "banTime = ?," + - "contacts = ?," + - "blocked = ?," + - "clanName = ?," + - "currentClan = ?," + - "clanReqs = ?," + - "timePlayed = ?," + - "lastLogin = ?," + - "online = ?" + - " WHERE uid = ?;" + private fun buildUpdateInfoQuery(updatedIndices: ArrayList) : String { + val sb = StringBuilder("UPDATE members SET ") + val validIndices = updatedIndices.filter { it in UPDATE_QUERY_FIELDS.keys } + for ((index, updatedIndex) in validIndices.withIndex()) { + sb.append(UPDATE_QUERY_FIELDS[updatedIndex] ?: continue) + sb.append(" = ?") + if (index < validIndices.size - 1) + sb.append(",") + } + sb.append(" WHERE uid = ?;") + return sb.toString() + } + + //Maps UserAccountInfo updated field indices to the column name for SQL. Mini ORM I guess? + private val UPDATE_QUERY_FIELDS = mapOf( + 0 to "username", + 1 to "password", + 3 to "rights", + 4 to "credits", + 6 to "lastGameIp", + 7 to "muteTime", + 8 to "banTime", + 9 to "contacts", + 10 to "blocked", + 11 to "clanName", + 12 to "currentClan", + 13 to "clanReqs", + 14 to "timePlayed", + 15 to "lastLogin", + 16 to "online" + ) } } diff --git a/Server/src/test/kotlin/core/storage/SQLStorageProviderTests.kt b/Server/src/test/kotlin/core/storage/SQLStorageProviderTests.kt index ab3895a29..ad76c795e 100644 --- a/Server/src/test/kotlin/core/storage/SQLStorageProviderTests.kt +++ b/Server/src/test/kotlin/core/storage/SQLStorageProviderTests.kt @@ -178,4 +178,24 @@ class SQLStorageProviderTests { val info3 = storage.getAccountInfo("dbupdateacc") Assertions.assertEquals(2, info3.rights) } + + @Test fun shouldCorrectlyUpdateMultipleChangedValues() { + val userData = UserAccountInfo.createDefault() + userData.username = "borpis2" + userData.password = "test" + testAccountNames.add("borpis2") + storage.store(userData) + + val lastLogin = System.currentTimeMillis() + + userData.credits = 2 + userData.lastLogin = lastLogin + userData.currentClan = "3009scape" + storage.update(userData) + + val data = storage.getAccountInfo(userData.username) + Assertions.assertEquals(2, data.credits, "Wrong data: $data") + Assertions.assertEquals(lastLogin, data.lastLogin, "Wrong data: $data") + Assertions.assertEquals("3009scape", data.currentClan, "Wrong data: $data") + } } \ No newline at end of file