diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/PlayerAccountLoader.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/PlayerAccountLoader.kt index 0dcd3bddf6..d9a56ec3bd 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/PlayerAccountLoader.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/PlayerAccountLoader.kt @@ -43,7 +43,7 @@ class PlayerAccountLoader( withContext(gameContext) { queue.await() logger.info { "Player logged in $username index $index." } - player.login(client, displayMode) + accounts.login(player, client, displayMode) } return player.instructions } catch (e: IllegalStateException) { diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/data/PlayerAccounts.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/data/PlayerAccounts.kt index f4589364f8..aaebc532a3 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/data/PlayerAccounts.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/data/PlayerAccounts.kt @@ -5,24 +5,37 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.mindrot.jbcrypt.BCrypt +import world.gregs.voidps.engine.client.ConnectionQueue import world.gregs.voidps.engine.client.ui.InterfaceOptions import world.gregs.voidps.engine.client.ui.Interfaces +import world.gregs.voidps.engine.client.ui.chat.plural +import world.gregs.voidps.engine.client.update.view.Viewport import world.gregs.voidps.engine.client.variable.PlayerVariables import world.gregs.voidps.engine.data.definition.* +import world.gregs.voidps.engine.entity.Despawn +import world.gregs.voidps.engine.entity.Spawn +import world.gregs.voidps.engine.entity.World +import world.gregs.voidps.engine.entity.character.mode.move.AreaEntered +import world.gregs.voidps.engine.entity.character.mode.move.AreaExited import world.gregs.voidps.engine.entity.character.move.previousTile -import world.gregs.voidps.engine.entity.character.player.Player -import world.gregs.voidps.engine.entity.character.player.PlayerOptions -import world.gregs.voidps.engine.entity.character.player.appearance -import world.gregs.voidps.engine.entity.character.player.name +import world.gregs.voidps.engine.entity.character.player.* import world.gregs.voidps.engine.entity.character.player.skill.level.PlayerLevels +import world.gregs.voidps.engine.get import world.gregs.voidps.engine.inv.equipment import world.gregs.voidps.engine.inv.restrict.ValidItemRestriction import world.gregs.voidps.engine.inv.stack.DependentOnItem import world.gregs.voidps.engine.map.collision.CollisionStrategyProvider +import world.gregs.voidps.engine.map.zone.RegionLoad +import world.gregs.voidps.engine.queue.strongQueue +import world.gregs.voidps.network.client.Client +import world.gregs.voidps.network.encode.login +import world.gregs.voidps.network.encode.logout import world.gregs.voidps.network.visual.PlayerVisuals import world.gregs.voidps.type.Direction import world.gregs.voidps.type.Tile +import java.util.concurrent.ConcurrentHashMap import kotlin.coroutines.CoroutineContext +import kotlin.system.measureTimeMillis class PlayerAccounts( private val interfaceDefinitions: InterfaceDefinitions, @@ -36,32 +49,37 @@ class PlayerAccounts( ) : Runnable, CoroutineScope { override val coroutineContext: CoroutineContext = Dispatchers.IO private val validItems = ValidItemRestriction(itemDefinitions) - private val saveQueue = mutableMapOf() + private val pending = ConcurrentHashMap() private val logger = InlineLogger() override fun run() { - if (saveQueue.isEmpty()) { + if (pending.isEmpty()) { return } - val accounts = saveQueue.values.toList() - saveQueue.clear() + val accounts = pending.values.toList() launch { try { - storage.save(accounts) + val took = measureTimeMillis { + storage.save(accounts) + for (account in accounts) { + pending.remove(account.name) + } + } + logger.info { "Saved ${accounts.size} ${"account".plural(accounts.size)} in ${took}ms" } } catch (e: Exception) { logger.error(e) { "Error saving players!" } } } } - fun queueSave(player: Player) { + fun save(player: Player) { if (player.contains("bot")) { return } - saveQueue[player.accountName] = player.copy() + pending[player.accountName] = player.copy() } - fun saving(name: String) = saveQueue.containsKey(name) + fun saving(name: String) = pending.containsKey(name) fun getOrElse(name: String, index: Int, block: () -> Player): Player { val player = storage.load(name)?.toPlayer() ?: block() @@ -102,4 +120,59 @@ class PlayerAccounts( player.collision = collisionStrategyProvider.get(character = player) } + fun login(player: Player, client: Client? = null, displayMode: Int = 0) { + player.interfaces.displayMode = displayMode + if (client != null) { + player.viewport = Viewport() + client.login(player.name, player.index, player.rights.ordinal, membersWorld = World.members) + player.client = client + player.interfaces.client = client + (player.variables as PlayerVariables).client = client + client.onDisconnecting { + logout(player, false) + } + } + player.emit(RegionLoad) + player.emit(Spawn) + val definitions = get() + for (def in definitions.get(player.tile.zone)) { + if (player.tile in def.area) { + player.emit(AreaEntered(player, def.name, def.tags, def.area)) + } + } + } + + fun logout(player: Player, safely: Boolean) { + if (player["logged_out", false]) { + return + } + player["logged_out"] = true + if (safely) { + player.client?.logout() + player.strongQueue("logout") { + // Make sure nothing else starts + } + } + player.client?.disconnect() + val queue: ConnectionQueue = get() + queue.disconnect { + val players: Players = get() + World.queue("logout", 1) { + players.remove(player) + players.removeIndex(player) + players.releaseIndex(player) + } + val definitions = get() + for (def in definitions.get(player.tile.zone)) { + if (player.tile in def.area) { + player.emit(AreaExited(player, def.name, def.tags, def.area)) + } + } + player.emit(Despawn) + player.queue.logout() + player.softTimers.stopAll() + player.timers.stopAll() + save(player) + } + } } \ No newline at end of file diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/player/Player.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/player/Player.kt index 050212feb4..262bbcfdda 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/player/Player.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/player/Player.kt @@ -2,39 +2,26 @@ package world.gregs.voidps.engine.entity.character.player import kotlinx.coroutines.flow.MutableSharedFlow import org.rsmod.game.pathfinder.collision.CollisionStrategy -import world.gregs.voidps.engine.client.ConnectionQueue import world.gregs.voidps.engine.client.ui.InterfaceOptions import world.gregs.voidps.engine.client.ui.Interfaces import world.gregs.voidps.engine.client.update.view.Viewport import world.gregs.voidps.engine.client.variable.PlayerVariables import world.gregs.voidps.engine.client.variable.Variables -import world.gregs.voidps.engine.data.PlayerAccounts -import world.gregs.voidps.engine.data.definition.AreaDefinitions -import world.gregs.voidps.engine.entity.Despawn -import world.gregs.voidps.engine.entity.Spawn -import world.gregs.voidps.engine.entity.World import world.gregs.voidps.engine.entity.character.Character import world.gregs.voidps.engine.entity.character.mode.EmptyMode import world.gregs.voidps.engine.entity.character.mode.Mode -import world.gregs.voidps.engine.entity.character.mode.move.AreaEntered -import world.gregs.voidps.engine.entity.character.mode.move.AreaExited import world.gregs.voidps.engine.entity.character.mode.move.Steps import world.gregs.voidps.engine.entity.character.player.chat.clan.ClanRank import world.gregs.voidps.engine.entity.character.player.equip.BodyParts import world.gregs.voidps.engine.entity.character.player.skill.exp.Experience import world.gregs.voidps.engine.entity.character.player.skill.level.Levels -import world.gregs.voidps.engine.get import world.gregs.voidps.engine.inv.Inventories -import world.gregs.voidps.engine.map.zone.RegionLoad import world.gregs.voidps.engine.queue.ActionQueue -import world.gregs.voidps.engine.queue.strongQueue import world.gregs.voidps.engine.suspend.Suspension import world.gregs.voidps.engine.timer.TimerQueue import world.gregs.voidps.engine.timer.Timers import world.gregs.voidps.network.Instruction import world.gregs.voidps.network.client.Client -import world.gregs.voidps.network.encode.login -import world.gregs.voidps.network.encode.logout import world.gregs.voidps.network.visual.PlayerVisuals import world.gregs.voidps.type.Tile import kotlin.coroutines.Continuation @@ -105,66 +92,6 @@ class Player( override val steps = Steps(this) - fun login(client: Client? = null, displayMode: Int = 0) { - interfaces.displayMode = displayMode - if (client != null) { - this.viewport = Viewport() - client.login(name, index, rights.ordinal, membersWorld = World.members) - this.client = client - interfaces.client = client - (variables as PlayerVariables).client = client - client.onDisconnecting { - logout(false) - } - } - emit(RegionLoad) - emit(Spawn) - val definitions = get() - for (def in definitions.get(tile.zone)) { - if (tile in def.area) { - emit(AreaEntered(this, def.name, def.tags, def.area)) - } - } - } - - fun logout(safely: Boolean) { - if (this["logged_out", false]) { - return - } - this["logged_out"] = true - if (safely) { - client?.logout() - strongQueue("logout") { - // Make sure nothing else starts - } - } - disconnect() - } - - private fun disconnect() { - client?.disconnect() - val queue: ConnectionQueue = get() - queue.disconnect { - val players: Players = get() - World.queue("logout", 1) { - players.remove(this@Player) - players.removeIndex(this@Player) - players.releaseIndex(this@Player) - } - val definitions = get() - for (def in definitions.get(tile.zone)) { - if (tile in def.area) { - emit(AreaExited(this@Player, def.name, def.tags, def.area)) - } - } - emit(Despawn) - this.queue.logout() - softTimers.stopAll() - timers.stopAll() - get().queueSave(this) - } - } - override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false diff --git a/engine/src/test/kotlin/world/gregs/voidps/engine/client/PlayerAccountLoaderTest.kt b/engine/src/test/kotlin/world/gregs/voidps/engine/client/PlayerAccountLoaderTest.kt index 2af88bfebd..2e5e1010a0 100644 --- a/engine/src/test/kotlin/world/gregs/voidps/engine/client/PlayerAccountLoaderTest.kt +++ b/engine/src/test/kotlin/world/gregs/voidps/engine/client/PlayerAccountLoaderTest.kt @@ -76,7 +76,7 @@ internal class PlayerAccountLoaderTest : KoinMock() { loader.load(client, "name", "pass", 2, 3) coVerify { - player.login(client, 3) + factory.login(player, client, 3) } } diff --git a/game/src/main/kotlin/world/gregs/voidps/bot/BotSpawns.kts b/game/src/main/kotlin/world/gregs/voidps/bot/BotSpawns.kts index 36bd5a89fd..d09a80f29a 100644 --- a/game/src/main/kotlin/world/gregs/voidps/bot/BotSpawns.kts +++ b/game/src/main/kotlin/world/gregs/voidps/bot/BotSpawns.kts @@ -98,7 +98,8 @@ fun spawn() { GlobalScope.launch(Contexts.Game) { val name = "Bot ${++counter}" val index = manager.add(name)!! - val bot = accounts.getOrElse(name, index) { Player(index = index, tile = lumbridge.random(), accountName = name) } + val bot = Player(index = index, tile = lumbridge.random(), accountName = name) + accounts.initPlayer(bot, index) setAppearance(bot) queue.await() if (bot.inventory.isEmpty()) { @@ -106,7 +107,7 @@ fun spawn() { } val client = if (TaskManager.DEBUG) DummyClient() else null bot.initBot() - bot.login(client, 0) + accounts.login(bot, client, 0) bot.emit(StartBot) bot.viewport?.loaded = true delay(3) diff --git a/game/src/main/kotlin/world/gregs/voidps/world/command/admin/AdminCommands.kts b/game/src/main/kotlin/world/gregs/voidps/world/command/admin/AdminCommands.kts index 8a27490f37..67988e831b 100644 --- a/game/src/main/kotlin/world/gregs/voidps/world/command/admin/AdminCommands.kts +++ b/game/src/main/kotlin/world/gregs/voidps/world/command/admin/AdminCommands.kts @@ -124,7 +124,7 @@ adminCommand("npc") { modCommand("save") { val account: PlayerAccounts = get() - players.forEach(account::queueSave) + players.forEach(account::save) } val definitions: ItemDefinitions by inject() diff --git a/game/src/main/kotlin/world/gregs/voidps/world/interact/entity/player/spawn/Exit.kts b/game/src/main/kotlin/world/gregs/voidps/world/interact/entity/player/spawn/Exit.kts index 638ae55c50..2cfed4c26e 100644 --- a/game/src/main/kotlin/world/gregs/voidps/world/interact/entity/player/spawn/Exit.kts +++ b/game/src/main/kotlin/world/gregs/voidps/world/interact/entity/player/spawn/Exit.kts @@ -3,16 +3,20 @@ package world.gregs.voidps.world.interact.entity.player.spawn import world.gregs.voidps.engine.client.message import world.gregs.voidps.engine.client.ui.interfaceOption import world.gregs.voidps.engine.client.ui.open +import world.gregs.voidps.engine.data.PlayerAccounts +import world.gregs.voidps.engine.inject import world.gregs.voidps.world.interact.entity.combat.inCombat interfaceOption("Exit", "logout", "toplevel*") { player.open("logout") } +val accounts: PlayerAccounts by inject() + interfaceOption(id = "logout") { if (player.inCombat) { player.message("You can't log out until 8 seconds after the end of combat.") return@interfaceOption } - player.logout(true) + accounts.logout(player, true) } \ No newline at end of file diff --git a/game/src/main/resources/game.properties b/game/src/main/resources/game.properties index 9eb49f15dc..ed6b464371 100644 --- a/game/src/main/resources/game.properties +++ b/game/src/main/resources/game.properties @@ -14,7 +14,7 @@ admin=Greg loginLimit=10 randomWalk=true characterCollision=true -loadUnusedObjects=true +loadUnusedObjects=false connectionPerTickCap=25 # Events shootingStars=true diff --git a/game/src/test/kotlin/world/gregs/voidps/world/script/WorldTest.kt b/game/src/test/kotlin/world/gregs/voidps/world/script/WorldTest.kt index a3fc7ac956..7e8fe79690 100644 --- a/game/src/test/kotlin/world/gregs/voidps/world/script/WorldTest.kt +++ b/game/src/test/kotlin/world/gregs/voidps/world/script/WorldTest.kt @@ -106,7 +106,7 @@ abstract class WorldTest : KoinTest { tick() player["creation"] = -1 player["skip_level_up"] = true - player.login(null, 0) + accounts.login(player, null, 0) player.softTimers.clear("restore_stats") player.softTimers.clear("restore_hitpoints") tick()