diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/EngineModules.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/EngineModules.kt index b79eb6595c..c27bb5bdee 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/EngineModules.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/EngineModules.kt @@ -5,8 +5,8 @@ import org.koin.dsl.module import org.rsmod.game.pathfinder.LineValidator import org.rsmod.game.pathfinder.PathFinder import org.rsmod.game.pathfinder.StepValidator -import world.gregs.voidps.engine.client.ConnectionGatekeeper import world.gregs.voidps.engine.client.ConnectionQueue +import world.gregs.voidps.engine.client.LoginManager import world.gregs.voidps.engine.client.update.batch.ZoneBatchUpdates import world.gregs.voidps.engine.data.PlayerAccounts import world.gregs.voidps.engine.data.definition.* @@ -48,7 +48,7 @@ val engineModule = module { single { ConnectionQueue(getIntProperty("connectionPerTickCap", 1)) } - single { ConnectionGatekeeper(get()) } + single { LoginManager(get().indexer) } single(createdAtStart = true) { GameObjectCollision(get()) } // Collision single { Collisions() } diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/ClientManager.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/ClientManager.kt new file mode 100644 index 0000000000..34c48f4234 --- /dev/null +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/ClientManager.kt @@ -0,0 +1,31 @@ +package world.gregs.voidps.engine.client + +import world.gregs.voidps.network.SessionManager +import java.util.concurrent.ConcurrentHashMap + +/** + * Keeps track of number of clients per ip address + */ +class ClientManager : SessionManager { + private val connections = ConcurrentHashMap() + + override fun count(key: String) = connections[key] ?: 0 + + override fun add(key: String): Int? { + connections[key] = count(key) + 1 + return null + } + + override fun remove(key: String) { + val count = count(key) - 1 + if (count <= 0) { + connections.remove(key) + } else { + connections[key] = count + } + } + + override fun clear() { + connections.clear() + } +} \ No newline at end of file diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/LoginManager.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/LoginManager.kt new file mode 100644 index 0000000000..7066d4506f --- /dev/null +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/LoginManager.kt @@ -0,0 +1,34 @@ +package world.gregs.voidps.engine.client + +import world.gregs.voidps.engine.client.ui.chat.toInt +import world.gregs.voidps.engine.entity.character.IndexAllocator +import world.gregs.voidps.network.SessionManager +import java.util.concurrent.ConcurrentHashMap + +/** + * Keeps track of the players online, prevents duplicate login attempts + */ +class LoginManager( + private val indices: IndexAllocator +) : SessionManager { + + private val online = ConcurrentHashMap.newKeySet() + + override fun count(key: String) = online.contains(key).toInt() + + override fun add(key: String): Int? { + if (!online.add(key)) { + return null + } + return indices.obtain() + } + + override fun remove(key: String) { + online.remove(key) + } + + override fun clear() { + indices.clear() + online.clear() + } +} \ No newline at end of file diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/Visuals.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/Visuals.kt index a2bb7e45ff..c8fd258482 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/Visuals.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/Visuals.kt @@ -9,18 +9,14 @@ import world.gregs.voidps.engine.entity.character.move.tele import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.appearance -import world.gregs.voidps.engine.entity.character.player.movementType import world.gregs.voidps.engine.entity.character.player.skill.Skill import world.gregs.voidps.engine.entity.obj.GameObject import world.gregs.voidps.engine.entity.obj.ObjectShape import world.gregs.voidps.engine.get -import world.gregs.voidps.engine.queue.strongQueue import world.gregs.voidps.network.visual.VisualMask import world.gregs.voidps.network.visual.Visuals import world.gregs.voidps.network.visual.update.Hitsplat import world.gregs.voidps.network.visual.update.Turn -import world.gregs.voidps.network.visual.update.player.MoveType -import world.gregs.voidps.network.visual.update.player.TemporaryMoveType import world.gregs.voidps.type.Delta import world.gregs.voidps.type.Direction import world.gregs.voidps.type.Distance @@ -34,7 +30,7 @@ fun Character.flagForceChat() = visuals.flag(if (this is Player) VisualMask.PLAY fun Character.flagHits() = visuals.flag(if (this is Player) VisualMask.PLAYER_HITS_MASK else VisualMask.NPC_HITS_MASK) -fun Character.flagForceMovement() = visuals.flag(if (this is Player) VisualMask.PLAYER_FORCE_MOVEMENT_MASK else VisualMask.NPC_FORCE_MOVEMENT_MASK) +fun Character.flagExactMovement() = visuals.flag(if (this is Player) VisualMask.PLAYER_EXACT_MOVEMENT_MASK else VisualMask.NPC_EXACT_MOVEMENT_MASK) fun Character.flagTurn() = visuals.flag(if (this is Player) VisualMask.PLAYER_TURN_MASK else VisualMask.NPC_TURN_MASK) @@ -176,14 +172,14 @@ private fun watchIndex(character: Character) = if (character is Player) characte * @param startDelay Client ticks until starting the movement * @param direction The cardinal direction to face during movement */ -fun Character.setForceMovement( +fun Character.setExactMovement( endDelta: Delta = Delta.EMPTY, endDelay: Int = 0, startDelta: Delta = Delta.EMPTY, startDelay: Int = 0, direction: Direction = Direction.NONE ) { - val move = visuals.forceMovement + val move = visuals.exactMovement check(endDelay > startDelay) { "End delay ($endDelay) must be after start delay ($startDelay)." } move.startX = startDelta.x move.startY = startDelta.y @@ -192,19 +188,19 @@ fun Character.setForceMovement( move.endY = endDelta.y move.endDelay = endDelay move.direction = direction.ordinal - flagForceMovement() + flagExactMovement() } -fun Character.forceWalk(delta: Delta, delay: Int = tile.distanceTo(tile.add(delta)) * 30, direction: Direction = Direction.NONE) { +fun Character.exactMove(delta: Delta, delay: Int = tile.distanceTo(tile.add(delta)) * 30, direction: Direction = Direction.NONE) { val start = tile tele(delta) - setForceMovement(Delta.EMPTY, delay, start.delta(tile), direction = direction) + setExactMovement(Delta.EMPTY, delay, start.delta(tile), direction = direction) } -fun Character.forceWalk(target: Tile, delay: Int = tile.distanceTo(target) * 30, direction: Direction = Direction.NONE) { +fun Character.exactMove(target: Tile, delay: Int = tile.distanceTo(target) * 30, direction: Direction = Direction.NONE) { val start = tile tele(target) - setForceMovement(Delta.EMPTY, delay, start.delta(tile), direction = direction) + setExactMovement(Delta.EMPTY, delay, start.delta(tile), direction = direction) } val Character.turn: Delta diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/suspend/Suspend.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/suspend/Suspend.kt index 25a9fa728e..7dc4834144 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/suspend/Suspend.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/suspend/Suspend.kt @@ -71,7 +71,7 @@ suspend fun CharacterContext.pauseForever() { } /** - * Movement delay, typically used by interactions that perform animations or force movements + * Movement delay, typically used by interactions that perform animations or exact movements */ suspend fun CharacterContext.arriveDelay() { val delay = character.remaining("last_movement") diff --git a/engine/src/test/kotlin/world/gregs/voidps/engine/client/ClientManagerTest.kt b/engine/src/test/kotlin/world/gregs/voidps/engine/client/ClientManagerTest.kt new file mode 100644 index 0000000000..0b4e68e407 --- /dev/null +++ b/engine/src/test/kotlin/world/gregs/voidps/engine/client/ClientManagerTest.kt @@ -0,0 +1,70 @@ +package world.gregs.voidps.engine.client + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class ClientManagerTest { + + private lateinit var manager: ClientManager + + @BeforeEach + fun setup() { + manager = ClientManager() + } + + @Test + fun `Same addresses are counted`() { + val address = "123.456.789" + assertEquals(0, manager.count(address)) + manager.add(address) + assertEquals(1, manager.count(address)) + manager.add(address) + assertEquals(2, manager.count(address)) + manager.add(address) + assertEquals(3, manager.count(address)) + } + + @Test + fun `Different addresses are separate`() { + assertEquals(0, manager.count("123.456.789")) + manager.add("123.456.789") + assertEquals(0, manager.count("100.000.000")) + manager.add("100.000.000") + + assertEquals(1, manager.count("100.000.000")) + assertEquals(1, manager.count("123.456.789")) + } + + @Test + fun `Disconnections aren't counted`() { + val address = "123.456.789" + assertEquals(0, manager.count(address)) + manager.add(address) + manager.add(address) + assertEquals(2, manager.count(address)) + manager.remove(address) + assertEquals(1, manager.count(address)) + } + + @Test + fun `Too many disconnections isn't negative`() { + val address = "123.456.789" + assertEquals(0, manager.count(address)) + manager.add(address) + assertEquals(1, manager.count(address)) + manager.remove(address) + assertEquals(0, manager.count(address)) + manager.remove(address) + assertEquals(0, manager.count(address)) + } + + @Test + fun `Clearing removes all counts`() { + val address = "123.456.789" + manager.add(address) + assertEquals(1, manager.count(address)) + manager.clear() + assertEquals(0, manager.count(address)) + } +} \ No newline at end of file diff --git a/engine/src/test/kotlin/world/gregs/voidps/engine/client/ConnectionGatekeeperTest.kt b/engine/src/test/kotlin/world/gregs/voidps/engine/client/ConnectionGatekeeperTest.kt deleted file mode 100644 index fc4d5c68b7..0000000000 --- a/engine/src/test/kotlin/world/gregs/voidps/engine/client/ConnectionGatekeeperTest.kt +++ /dev/null @@ -1,55 +0,0 @@ -package world.gregs.voidps.engine.client - -import io.mockk.every -import io.mockk.mockk -import io.mockk.spyk -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.koin.dsl.module -import world.gregs.voidps.engine.entity.character.IndexAllocator -import world.gregs.voidps.engine.entity.character.player.Players -import world.gregs.voidps.engine.getIntProperty -import world.gregs.voidps.engine.script.KoinMock -import world.gregs.voidps.network.NetworkGatekeeper - -internal class ConnectionGatekeeperTest : KoinMock() { - - private lateinit var gatekeeper: NetworkGatekeeper - private lateinit var players: Players - - override val modules = listOf( - module { - single { - ConnectionQueue(getIntProperty("connectionPerTickCap", 1)) - } - single { ConnectionGatekeeper(get()) } - } - ) - - @BeforeEach - fun setup() { - players = mockk(relaxed = true) - every { players.indexer } returns IndexAllocator(5) - gatekeeper = spyk(ConnectionGatekeeper(players)) - } - - @Test - fun `Login player name`() { - val index = gatekeeper.connect("test", "123") - - assertEquals(1, index) - assertEquals(1, gatekeeper.connections("123")) - assertEquals(0, gatekeeper.connections("321")) - assertTrue(gatekeeper.connected("test")) - assertFalse(gatekeeper.connected("not online")) - } - - @Test - fun `Logout player not online`() { - gatekeeper.connect("test", "123") - gatekeeper.disconnect("test", "123") - assertEquals(0, gatekeeper.connections("123")) - assertFalse(gatekeeper.connected("test")) - } -} \ No newline at end of file diff --git a/engine/src/test/kotlin/world/gregs/voidps/engine/client/ConnectionQueueTest.kt b/engine/src/test/kotlin/world/gregs/voidps/engine/client/ConnectionQueueTest.kt index ffca78556a..c0c69c28d1 100644 --- a/engine/src/test/kotlin/world/gregs/voidps/engine/client/ConnectionQueueTest.kt +++ b/engine/src/test/kotlin/world/gregs/voidps/engine/client/ConnectionQueueTest.kt @@ -22,7 +22,6 @@ internal class ConnectionQueueTest : KoinMock() { single { ConnectionQueue(getIntProperty("connectionPerTickCap", 1)) } - single { ConnectionGatekeeper(get()) } } ) diff --git a/engine/src/test/kotlin/world/gregs/voidps/engine/client/LoginManagerTest.kt b/engine/src/test/kotlin/world/gregs/voidps/engine/client/LoginManagerTest.kt new file mode 100644 index 0000000000..abff9ae6eb --- /dev/null +++ b/engine/src/test/kotlin/world/gregs/voidps/engine/client/LoginManagerTest.kt @@ -0,0 +1,70 @@ +package world.gregs.voidps.engine.client + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import world.gregs.voidps.engine.entity.character.IndexAllocator + +class LoginManagerTest { + + private lateinit var manager: LoginManager + private lateinit var indices: IndexAllocator + + @BeforeEach + fun setup() { + indices = IndexAllocator(5) + manager = LoginManager(indices) + } + + @Test + fun `Same names can't be added twice`() { + val name = "player" + assertEquals(0, manager.count(name)) + assertEquals(1, manager.add(name)) + assertEquals(1, manager.count(name)) + assertNull(manager.add(name)) + assertEquals(1, manager.count(name)) + } + + @Test + fun `Different names different count`() { + val name1 = "player1" + val name2 = "player2" + assertEquals(0, manager.count(name1)) + assertEquals(1, manager.add(name1)) + assertEquals(1, manager.count(name1)) + assertEquals(0, manager.count(name2)) + assertEquals(2, manager.add(name2)) + assertEquals(1, manager.count(name2)) + } + + @Test + fun `Removed named clear count`() { + val name = "player" + assertEquals(1, manager.add(name)) + assertEquals(1, manager.count(name)) + manager.remove(name) + assertEquals(0, manager.count(name)) + } + + @Test + fun `Removing name twice doesn't cause negative count`() { + val name = "player" + assertEquals(1, manager.add(name)) + assertEquals(1, manager.count(name)) + manager.remove(name) + assertEquals(0, manager.count(name)) + manager.remove(name) + assertEquals(0, manager.count(name)) + } + + @Test + fun `Clear names removes count`() { + val name = "player" + assertEquals(1, manager.add(name)) + assertEquals(1, manager.count(name)) + manager.clear() + assertEquals(0, manager.count(name)) + assertEquals(1, manager.add(name)) + } +} \ No newline at end of file diff --git a/game/src/main/kotlin/world/gregs/voidps/GameTick.kt b/game/src/main/kotlin/world/gregs/voidps/GameTick.kt index 9172191785..a5f3b93fa4 100644 --- a/game/src/main/kotlin/world/gregs/voidps/GameTick.kt +++ b/game/src/main/kotlin/world/gregs/voidps/GameTick.kt @@ -3,10 +3,12 @@ package world.gregs.voidps import world.gregs.voidps.engine.client.ConnectionQueue import world.gregs.voidps.engine.client.instruction.InstructionTask import world.gregs.voidps.engine.client.instruction.InterfaceHandler +import world.gregs.voidps.engine.client.update.CharacterTask import world.gregs.voidps.engine.client.update.CharacterUpdateTask import world.gregs.voidps.engine.client.update.NPCTask import world.gregs.voidps.engine.client.update.PlayerTask import world.gregs.voidps.engine.client.update.batch.ZoneBatchUpdates +import world.gregs.voidps.engine.client.update.iterator.ParallelIterator import world.gregs.voidps.engine.client.update.iterator.SequentialIterator import world.gregs.voidps.engine.client.update.iterator.TaskIterator import world.gregs.voidps.engine.client.update.npc.NPCResetTask @@ -58,10 +60,11 @@ fun getTickStages( interfaceDefinitions: InterfaceDefinitions = get(), hunting: Hunting = get(), handler: InterfaceHandler = InterfaceHandler(get(), get(), get()), - iterator: TaskIterator + sequential: Boolean = CharacterTask.DEBUG ): List { val sequentialNpc: TaskIterator = SequentialIterator() val sequentialPlayer: TaskIterator = SequentialIterator() + val iterator: TaskIterator = if (sequential) SequentialIterator() else ParallelIterator() return listOf( PlayerResetTask(sequentialPlayer, players, batches), NPCResetTask(sequentialNpc, npcs), @@ -101,7 +104,7 @@ private fun playerVisualEncoders() = castOf( ForceChatEncoder(PLAYER_FORCE_CHAT_MASK), PlayerHitsEncoder(), PlayerTurnEncoder(), - PlayerForceMovementEncoder(), + PlayerExactMovementEncoder(), PlayerSecondaryGraphicEncoder(), PlayerColourOverlayEncoder(), TemporaryMoveTypeEncoder(), @@ -116,7 +119,7 @@ private fun npcVisualEncoders() = castOf( NPCAnimationEncoder(), NPCPrimaryGraphicEncoder(), NPCTurnEncoder(), - NPCForceMovementEncoder(), + NPCExactMovementEncoder(), NPCColourOverlayEncoder(), NPCHitsEncoder(), WatchEncoder(NPC_WATCH_MASK), diff --git a/game/src/main/kotlin/world/gregs/voidps/Main.kt b/game/src/main/kotlin/world/gregs/voidps/Main.kt index d7852faad3..489fab05b5 100644 --- a/game/src/main/kotlin/world/gregs/voidps/Main.kt +++ b/game/src/main/kotlin/world/gregs/voidps/Main.kt @@ -1,6 +1,7 @@ package world.gregs.voidps import com.github.michaelbull.logging.InlineLogger +import kotlinx.coroutines.* import org.koin.core.context.startKoin import org.koin.core.logger.Level import org.koin.dsl.module @@ -13,29 +14,26 @@ import world.gregs.voidps.cache.config.decoder.StructDecoder import world.gregs.voidps.cache.definition.decoder.* import world.gregs.voidps.cache.secure.Huffman import world.gregs.voidps.engine.* -import world.gregs.voidps.engine.client.ConnectionGatekeeper -import world.gregs.voidps.engine.client.ConnectionQueue -import world.gregs.voidps.engine.client.PlayerAccountLoader -import world.gregs.voidps.engine.client.update.CharacterTask -import world.gregs.voidps.engine.client.update.iterator.ParallelIterator -import world.gregs.voidps.engine.client.update.iterator.SequentialIterator +import world.gregs.voidps.engine.client.* import world.gregs.voidps.engine.data.definition.* import world.gregs.voidps.engine.entity.World +import world.gregs.voidps.engine.entity.character.player.Players import world.gregs.voidps.engine.map.collision.CollisionDecoder import world.gregs.voidps.network.GameServer import world.gregs.voidps.network.LoginServer import world.gregs.voidps.network.protocol import world.gregs.voidps.script.loadScripts import java.io.File -import java.net.BindException import java.util.* +import kotlin.coroutines.CoroutineContext /** * @author GregHib * @since April 18, 2020 */ -object Main { +object Main : CoroutineScope { + override val coroutineContext: CoroutineContext = Contexts.Game lateinit var name: String private val logger = InlineLogger() private const val PROPERTY_FILE_NAME = "game.properties" @@ -47,29 +45,33 @@ object Main { val properties = properties() name = properties.getProperty("name") + // File server val cache = timed("cache") { Cache.load(properties) } + val server = GameServer.load(cache, properties, ClientManager()) + val job = server.start(properties.getProperty("port").toInt()) + + // Content preload(cache, properties) - val accountLoader = PlayerAccountLoader(get(), get(), Contexts.Game) + // Login server val protocol = protocol(get()) + val accounts: LoginManager = get() + val accountLoader = PlayerAccountLoader(get(), get(), Contexts.Game) + val loginServer = LoginServer.load(properties, protocol, accounts, accountLoader) - val gatekeeper: ConnectionGatekeeper = get() - val loginServer = LoginServer.load(properties, protocol, gatekeeper, accountLoader) - val server = GameServer.load(cache, properties, gatekeeper, loginServer) - - val tickStages = getTickStages(iterator = if (CharacterTask.DEBUG) SequentialIterator() else ParallelIterator()) - val engine = GameLoop(tickStages) + // Game world + val stages = getTickStages() + val engine = GameLoop(stages) World.start(properties) engine.start() + server.loginServer = loginServer logger.info { "$name loaded in ${System.currentTimeMillis() - startTime}ms" } - - try { - server.start(getIntProperty("port")) - } catch (e: BindException) { - logger.error(e) { "Error starting server." } - } finally { - server.stop() - engine.stop() + runBlocking { + try { + job.join() + } finally { + engine.stop() + } } } 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 4d954a46c2..31f213ad87 100644 --- a/game/src/main/kotlin/world/gregs/voidps/bot/BotSpawns.kts +++ b/game/src/main/kotlin/world/gregs/voidps/bot/BotSpawns.kts @@ -2,8 +2,8 @@ package world.gregs.voidps.bot import kotlinx.coroutines.* import world.gregs.voidps.engine.Contexts -import world.gregs.voidps.engine.client.ConnectionGatekeeper import world.gregs.voidps.engine.client.ConnectionQueue +import world.gregs.voidps.engine.client.LoginManager import world.gregs.voidps.engine.client.ui.event.adminCommand import world.gregs.voidps.engine.data.PlayerAccounts import world.gregs.voidps.engine.data.definition.AreaDefinitions @@ -38,7 +38,7 @@ val botCount = getIntProperty("bots", 0) val bots = mutableListOf() val queue: ConnectionQueue by inject() -val gatekeeper: ConnectionGatekeeper by inject() +val manager: LoginManager by inject() val accounts: PlayerAccounts by inject() val enums: EnumDefinitions by inject() val structs: StructDefinitions by inject() @@ -97,7 +97,7 @@ adminCommand("bot") { fun spawn() { GlobalScope.launch(Contexts.Game) { val name = "Bot ${++counter}" - val index = gatekeeper.connect(name)!! + val index = manager.add(name)!! val bot = accounts.getOrElse(name, index) { Player(index = index, tile = lumbridge.random(), accountName = name) } setAppearance(bot) queue.await() diff --git a/game/src/main/kotlin/world/gregs/voidps/world/activity/dnd/shootingstar/ShootingStar.kts b/game/src/main/kotlin/world/gregs/voidps/world/activity/dnd/shootingstar/ShootingStar.kts index 8bbb2bb17e..6c5cd1cb1c 100644 --- a/game/src/main/kotlin/world/gregs/voidps/world/activity/dnd/shootingstar/ShootingStar.kts +++ b/game/src/main/kotlin/world/gregs/voidps/world/activity/dnd/shootingstar/ShootingStar.kts @@ -6,7 +6,7 @@ import world.gregs.voidps.engine.client.ui.chat.plural import world.gregs.voidps.engine.client.variable.start import world.gregs.voidps.engine.data.definition.data.Rock import world.gregs.voidps.engine.entity.World -import world.gregs.voidps.engine.entity.character.forceWalk +import world.gregs.voidps.engine.entity.character.exactMove import world.gregs.voidps.engine.entity.character.mode.interact.Interact import world.gregs.voidps.engine.entity.character.move.walkTo import world.gregs.voidps.engine.entity.character.npc.NPC @@ -80,7 +80,7 @@ fun startCrashedStarEvent() { for (player in under) { player.damage(random.nextInt(10, 50)) val direction = Direction.all.first { !player.blocked(it) } - player.forceWalk(direction.delta, 1, direction = direction.inverse()) + player.exactMove(direction.delta, 1, direction = direction.inverse()) player.setAnimation("step_back_startled") } World.queue("falling_star_object_removal", 1) { diff --git a/game/src/main/kotlin/world/gregs/voidps/world/command/debug/PlayerUpdatingCommands.kts b/game/src/main/kotlin/world/gregs/voidps/world/command/debug/PlayerUpdatingCommands.kts index 2bb2daa260..a4dc1f92e5 100644 --- a/game/src/main/kotlin/world/gregs/voidps/world/command/debug/PlayerUpdatingCommands.kts +++ b/game/src/main/kotlin/world/gregs/voidps/world/command/debug/PlayerUpdatingCommands.kts @@ -83,7 +83,7 @@ adminCommand("chat") { } adminCommand("move") { - player.setForceMovement(Delta(0, -2), 120, startDelay = 60, direction = Direction.SOUTH) + player.setExactMovement(Delta(0, -2), 120, startDelay = 60, direction = Direction.SOUTH) } adminCommand("hit") { diff --git a/game/src/main/kotlin/world/gregs/voidps/world/interact/entity/player/combat/melee/special/SpearShove.kts b/game/src/main/kotlin/world/gregs/voidps/world/interact/entity/player/combat/melee/special/SpearShove.kts index 609631ef75..33a16697df 100644 --- a/game/src/main/kotlin/world/gregs/voidps/world/interact/entity/player/combat/melee/special/SpearShove.kts +++ b/game/src/main/kotlin/world/gregs/voidps/world/interact/entity/player/combat/melee/special/SpearShove.kts @@ -3,7 +3,7 @@ package world.gregs.voidps.world.interact.entity.player.combat.melee.special import world.gregs.voidps.engine.client.message import world.gregs.voidps.engine.client.variable.hasClock import world.gregs.voidps.engine.client.variable.start -import world.gregs.voidps.engine.entity.character.forceWalk +import world.gregs.voidps.engine.entity.character.exactMove import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.setAnimation import world.gregs.voidps.engine.entity.character.setGraphic @@ -43,7 +43,7 @@ specialAttackSwing("dragon_spear", "zamorakian_spear") { player -> val direction = target.tile.delta(actual).toDirection() val delta = direction.delta if (!target.blocked(direction)) { - target.forceWalk(delta, 30, direction.inverse()) + target.exactMove(delta, 30, direction.inverse()) } delay = 4 } \ No newline at end of file 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 75a5ab5a77..bdb3739df0 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 @@ -19,11 +19,10 @@ import world.gregs.voidps.cache.config.decoder.StructDecoder import world.gregs.voidps.cache.definition.decoder.* import world.gregs.voidps.cache.secure.Huffman import world.gregs.voidps.engine.* -import world.gregs.voidps.engine.client.ConnectionGatekeeper import world.gregs.voidps.engine.client.ConnectionQueue +import world.gregs.voidps.engine.client.LoginManager import world.gregs.voidps.engine.client.instruction.InterfaceHandler import world.gregs.voidps.engine.client.update.batch.ZoneBatchUpdates -import world.gregs.voidps.engine.client.update.iterator.SequentialIterator import world.gregs.voidps.engine.client.update.view.Viewport import world.gregs.voidps.engine.data.PlayerAccounts import world.gregs.voidps.engine.data.definition.* @@ -45,7 +44,6 @@ import world.gregs.voidps.engine.map.collision.Collisions import world.gregs.voidps.engine.map.collision.GameObjectCollision import world.gregs.voidps.gameModule import world.gregs.voidps.getTickStages -import world.gregs.voidps.network.NetworkGatekeeper import world.gregs.voidps.network.client.Client import world.gregs.voidps.script.loadScripts import world.gregs.voidps.type.Tile @@ -64,7 +62,7 @@ abstract class WorldTest : KoinTest { private val logger = InlineLogger() private lateinit var engine: GameLoop lateinit var players: Players - private lateinit var gatekeeper: NetworkGatekeeper + private lateinit var manager: LoginManager lateinit var npcs: NPCs lateinit var floorItems: FloorItems lateinit var objects: GameObjects @@ -101,7 +99,7 @@ abstract class WorldTest : KoinTest { fun createPlayer(name: String, tile: Tile = Tile.EMPTY): Player { val accounts: PlayerAccounts = get() - val index = gatekeeper.connect(name)!! + val index = players.indexer.obtain()!! val player = Player(tile = tile, accountName = name, passwordHash = "") accounts.initPlayer(player, index) accountDefs.add(player) @@ -184,11 +182,11 @@ abstract class WorldTest : KoinTest { get(), get(), handler, - iterator = SequentialIterator()) + sequential = true) engine = GameLoop(tickStages, mockk(relaxed = true)) World.start(true) } - gatekeeper = get() + manager = get() players = get() npcs = get() floorItems = get() @@ -210,7 +208,7 @@ abstract class WorldTest : KoinTest { @AfterEach fun afterEach() { - gatekeeper.clear() + manager.clear() players.clear() npcs.clear() floorItems.clear() diff --git a/network/src/main/kotlin/world/gregs/voidps/network/GameServer.kt b/network/src/main/kotlin/world/gregs/voidps/network/GameServer.kt index d5e733e48c..3495295e33 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/GameServer.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/GameServer.kt @@ -8,6 +8,7 @@ import io.ktor.utils.io.* import kotlinx.coroutines.* import kotlinx.coroutines.channels.ClosedReceiveChannelException import world.gregs.voidps.cache.Cache +import java.net.BindException import java.net.SocketException import java.util.* import java.util.concurrent.Executors @@ -17,16 +18,16 @@ import kotlin.concurrent.thread * A network server for client's to connect to the game with */ class GameServer( - private val gatekeeper: NetworkGatekeeper, + private val clients: SessionManager, private val loginLimit: Int, - private val loginServer: Server, private val fileServer: Server ) { private lateinit var dispatcher: ExecutorCoroutineDispatcher private var running = false + var loginServer: Server? = null - fun start(port: Int) { + fun start(port: Int): Job { Runtime.getRuntime().addShutdownHook(thread(start = false) { stop() }) val executor = Executors.newCachedThreadPool() dispatcher = executor.asCoroutineDispatcher() @@ -41,10 +42,15 @@ class GameServer( logger.error(throwable) { "Connection error" } } } - runBlocking { - val scope = CoroutineScope(supervisor + exceptionHandler) - with(scope) { - val server = aSocket(selector).tcp().bind(port = port) + val scope = CoroutineScope(supervisor + exceptionHandler) + val server = try { + aSocket(selector).tcp().bind(port = port) + } catch (exception: BindException) { + stop() + throw exception + } + return scope.launch { + try { running = true logger.info { "Listening for requests on port ${port}..." } while (running) { @@ -56,22 +62,30 @@ class GameServer( connect(read, write, socket.remoteAddress.toJavaAddress().hostname) } } + } finally { + stop() } } } suspend fun connect(read: ByteReadChannel, write: ByteWriteChannel, hostname: String) { - if (gatekeeper.connections(hostname) >= loginLimit) { + if (clients.count(hostname) >= loginLimit) { write.finish(Response.LOGIN_LIMIT_EXCEEDED) return } - when (val opcode = read.readByte().toInt()) { - Request.CONNECT_LOGIN -> loginServer.connect(read, write, hostname) - Request.CONNECT_JS5 -> fileServer.connect(read, write, hostname) - else -> { - logger.trace { "Invalid sync session id: $opcode" } - write.finish(Response.INVALID_LOGIN_SERVER) + try { + clients.add(hostname) + when (val opcode = read.readByte().toInt()) { + Request.CONNECT_LOGIN -> loginServer?.connect(read, write, hostname) + ?: write.respond(Response.LOGIN_SERVER_OFFLINE) + Request.CONNECT_JS5 -> fileServer.connect(read, write, hostname) + else -> { + logger.trace { "Invalid sync session id: $opcode" } + write.finish(Response.INVALID_LOGIN_SERVER) + } } + } finally { + clients.remove(hostname) } } @@ -83,10 +97,10 @@ class GameServer( companion object { @ExperimentalUnsignedTypes - fun load(cache: Cache, properties: Properties, gatekeeper: NetworkGatekeeper, loginServer: LoginServer): GameServer { + fun load(cache: Cache, properties: Properties, clients: SessionManager): GameServer { val limit = properties.getProperty("loginLimit").toInt() val fileServer = FileServer.load(cache, properties) - return GameServer(gatekeeper, limit, loginServer, fileServer) + return GameServer(clients, limit, fileServer) } private val logger = InlineLogger() diff --git a/network/src/main/kotlin/world/gregs/voidps/network/LoginServer.kt b/network/src/main/kotlin/world/gregs/voidps/network/LoginServer.kt index ee003d01ff..6c01d6e7fb 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/LoginServer.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/LoginServer.kt @@ -20,7 +20,7 @@ class LoginServer( private val revision: Int, private val modulus: BigInteger, private val private: BigInteger, - private val gatekeeper: NetworkGatekeeper, + private val accounts: SessionManager, private val loader: AccountLoader ) : Server { @@ -78,7 +78,7 @@ class LoginServer( val xtea = decryptXtea(packet, isaacKeys) val username = xtea.readString() - if (gatekeeper.connected(username)) { + if (accounts.count(username) > 0) { write.finish(Response.ACCOUNT_ONLINE) return } @@ -105,9 +105,9 @@ class LoginServer( } suspend fun login(read: ByteReadChannel, client: Client, username: String, password: String, displayMode: Int) { - val index = gatekeeper.connect(username, client.address) + val index = accounts.add(username) client.onDisconnected { - gatekeeper.disconnect(username, client.address) + accounts.remove(username) } if (index == null) { client.disconnect(Response.WORLD_FULL) @@ -143,11 +143,11 @@ class LoginServer( companion object { private val logger = InlineLogger() - fun load(properties: Properties, protocol: Array, gatekeeper: NetworkGatekeeper, loader: AccountLoader): LoginServer { + fun load(properties: Properties, protocol: Array, accounts: SessionManager, loader: AccountLoader): LoginServer { val gameModulus = BigInteger(properties.getProperty("gameModulus"), 16) val gamePrivate = BigInteger(properties.getProperty("gamePrivate"), 16) val revision = properties.getProperty("revision").toInt() - return LoginServer(protocol, revision, gameModulus, gamePrivate, gatekeeper, loader) + return LoginServer(protocol, revision, gameModulus, gamePrivate, accounts, loader) } } } \ No newline at end of file diff --git a/network/src/main/kotlin/world/gregs/voidps/network/SessionManager.kt b/network/src/main/kotlin/world/gregs/voidps/network/SessionManager.kt new file mode 100644 index 0000000000..51469a513a --- /dev/null +++ b/network/src/main/kotlin/world/gregs/voidps/network/SessionManager.kt @@ -0,0 +1,8 @@ +package world.gregs.voidps.network + +interface SessionManager { + fun count(key: String): Int + fun add(key: String): Int? + fun remove(key: String) + fun clear() +} \ No newline at end of file diff --git a/network/src/main/kotlin/world/gregs/voidps/network/visual/VisualEncoder.kt b/network/src/main/kotlin/world/gregs/voidps/network/visual/VisualEncoder.kt index 7d3d9952a3..3f48a1c072 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/visual/VisualEncoder.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/visual/VisualEncoder.kt @@ -5,7 +5,6 @@ import world.gregs.voidps.buffer.write.Writer abstract class VisualEncoder( val mask: Int, val initial: Boolean = false, - val appearance: Boolean = false ) { /** diff --git a/network/src/main/kotlin/world/gregs/voidps/network/visual/VisualMask.kt b/network/src/main/kotlin/world/gregs/voidps/network/visual/VisualMask.kt index a40607c494..62cddec0f2 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/visual/VisualMask.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/visual/VisualMask.kt @@ -7,7 +7,7 @@ object VisualMask { const val PLAYER_FORCE_CHAT_MASK = 0x1000 const val PLAYER_HITS_MASK = 0x4 const val PLAYER_TURN_MASK = 0x2 - const val PLAYER_FORCE_MOVEMENT_MASK = 0x2000 + const val PLAYER_EXACT_MOVEMENT_MASK = 0x2000 const val PLAYER_GRAPHIC_2_MASK = 0x200 const val PLAYER_COLOUR_OVERLAY_MASK = 0x40000 const val TEMPORARY_MOVE_TYPE_MASK = 0x80 @@ -21,7 +21,7 @@ object VisualMask { const val NPC_ANIMATION_MASK = 0x8 const val NPC_GRAPHIC_1_MASK = 0x20 const val NPC_TURN_MASK = 0x4 - const val NPC_FORCE_MOVEMENT_MASK = 0x1000 + const val NPC_EXACT_MOVEMENT_MASK = 0x1000 const val NPC_COLOUR_OVERLAY_MASK = 0x2000 const val NPC_HITS_MASK = 0x40 const val NPC_WATCH_MASK = 0x80 diff --git a/network/src/main/kotlin/world/gregs/voidps/network/visual/Visuals.kt b/network/src/main/kotlin/world/gregs/voidps/network/visual/Visuals.kt index fe1765fc03..c10af16d0c 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/visual/Visuals.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/visual/Visuals.kt @@ -15,7 +15,7 @@ abstract class Visuals(index: Int) { val primaryGraphic = Graphic() val secondaryGraphic = Graphic() val colourOverlay = ColourOverlay() - val forceMovement = ForceMovement() + val exactMovement = ExactMovement() val timeBar = TimeBar() val turn = Turn() val watch = Watch() @@ -37,7 +37,7 @@ abstract class Visuals(index: Int) { flag = 0 animation.clear() primaryGraphic.clear() - forceMovement.clear() + exactMovement.clear() colourOverlay.clear() hits.clear() turn.clear() diff --git a/network/src/main/kotlin/world/gregs/voidps/network/visual/encode/npc/NPCForceMovementEncoder.kt b/network/src/main/kotlin/world/gregs/voidps/network/visual/encode/npc/NPCExactMovementEncoder.kt similarity index 78% rename from network/src/main/kotlin/world/gregs/voidps/network/visual/encode/npc/NPCForceMovementEncoder.kt rename to network/src/main/kotlin/world/gregs/voidps/network/visual/encode/npc/NPCExactMovementEncoder.kt index 257c7e0c18..332cadeebd 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/visual/encode/npc/NPCForceMovementEncoder.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/visual/encode/npc/NPCExactMovementEncoder.kt @@ -3,12 +3,12 @@ package world.gregs.voidps.network.visual.encode.npc import world.gregs.voidps.buffer.write.Writer import world.gregs.voidps.network.visual.NPCVisuals import world.gregs.voidps.network.visual.VisualEncoder -import world.gregs.voidps.network.visual.VisualMask.NPC_FORCE_MOVEMENT_MASK +import world.gregs.voidps.network.visual.VisualMask.NPC_EXACT_MOVEMENT_MASK -class NPCForceMovementEncoder : VisualEncoder(NPC_FORCE_MOVEMENT_MASK) { +class NPCExactMovementEncoder : VisualEncoder(NPC_EXACT_MOVEMENT_MASK) { override fun encode(writer: Writer, visuals: NPCVisuals) { - val (tile1X, tile1Y, delay1, tile2X, tile2Y, delay2, direction) = visuals.forceMovement + val (tile1X, tile1Y, delay1, tile2X, tile2Y, delay2, direction) = visuals.exactMovement writer.apply { writeByteSubtract(tile1X) writeByteSubtract(tile1Y) diff --git a/network/src/main/kotlin/world/gregs/voidps/network/visual/encode/player/AppearanceEncoder.kt b/network/src/main/kotlin/world/gregs/voidps/network/visual/encode/player/AppearanceEncoder.kt index 9b496c56a0..fb67567071 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/visual/encode/player/AppearanceEncoder.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/visual/encode/player/AppearanceEncoder.kt @@ -6,7 +6,7 @@ import world.gregs.voidps.network.visual.VisualEncoder import world.gregs.voidps.network.visual.VisualMask.APPEARANCE_MASK import world.gregs.voidps.network.visual.update.player.Appearance -class AppearanceEncoder : VisualEncoder(APPEARANCE_MASK, initial = true, appearance = true) { +class AppearanceEncoder : VisualEncoder(APPEARANCE_MASK, initial = true) { override fun encode(writer: Writer, visuals: PlayerVisuals) { val (showSkillLevel, diff --git a/network/src/main/kotlin/world/gregs/voidps/network/visual/encode/player/PlayerForceMovementEncoder.kt b/network/src/main/kotlin/world/gregs/voidps/network/visual/encode/player/PlayerExactMovementEncoder.kt similarity index 77% rename from network/src/main/kotlin/world/gregs/voidps/network/visual/encode/player/PlayerForceMovementEncoder.kt rename to network/src/main/kotlin/world/gregs/voidps/network/visual/encode/player/PlayerExactMovementEncoder.kt index 48ff685b62..37b0638737 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/visual/encode/player/PlayerForceMovementEncoder.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/visual/encode/player/PlayerExactMovementEncoder.kt @@ -3,12 +3,12 @@ package world.gregs.voidps.network.visual.encode.player import world.gregs.voidps.buffer.write.Writer import world.gregs.voidps.network.visual.PlayerVisuals import world.gregs.voidps.network.visual.VisualEncoder -import world.gregs.voidps.network.visual.VisualMask.PLAYER_FORCE_MOVEMENT_MASK +import world.gregs.voidps.network.visual.VisualMask.PLAYER_EXACT_MOVEMENT_MASK -class PlayerForceMovementEncoder : VisualEncoder(PLAYER_FORCE_MOVEMENT_MASK) { +class PlayerExactMovementEncoder : VisualEncoder(PLAYER_EXACT_MOVEMENT_MASK) { override fun encode(writer: Writer, visuals: PlayerVisuals) { - val (tile1X, tile1Y, delay1, tile2X, tile2Y, delay2, direction) = visuals.forceMovement + val (tile1X, tile1Y, delay1, tile2X, tile2Y, delay2, direction) = visuals.exactMovement writer.apply { writeByte(tile1X) writeByteSubtract(tile1Y) diff --git a/network/src/main/kotlin/world/gregs/voidps/network/visual/update/Animation.kt b/network/src/main/kotlin/world/gregs/voidps/network/visual/update/Animation.kt index 860c48e3a3..3863a171e6 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/visual/update/Animation.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/visual/update/Animation.kt @@ -3,8 +3,8 @@ package world.gregs.voidps.network.visual.update import world.gregs.voidps.network.Visual /** - * @param stand animate only while stationary (or during force movement) - * @param force animate after force movement + * @param stand animate only while stationary (or during exact movement) + * @param force animate after exact movement * @param walk can animate while walking * @param run can animate while running */ diff --git a/network/src/main/kotlin/world/gregs/voidps/network/visual/update/ForceMovement.kt b/network/src/main/kotlin/world/gregs/voidps/network/visual/update/ExactMovement.kt similarity index 88% rename from network/src/main/kotlin/world/gregs/voidps/network/visual/update/ForceMovement.kt rename to network/src/main/kotlin/world/gregs/voidps/network/visual/update/ExactMovement.kt index a3464103a9..00bffb1651 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/visual/update/ForceMovement.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/visual/update/ExactMovement.kt @@ -2,7 +2,7 @@ package world.gregs.voidps.network.visual.update import world.gregs.voidps.network.Visual -data class ForceMovement( +data class ExactMovement( var startX: Int = 0, var startY: Int = 0, var startDelay: Int = 0, diff --git a/network/src/test/kotlin/world/gregs/voidps/network/GameServerTest.kt b/network/src/test/kotlin/world/gregs/voidps/network/GameServerTest.kt index 2bb468f88d..6d3a9987d1 100644 --- a/network/src/test/kotlin/world/gregs/voidps/network/GameServerTest.kt +++ b/network/src/test/kotlin/world/gregs/voidps/network/GameServerTest.kt @@ -17,7 +17,7 @@ internal class GameServerTest { lateinit var server: GameServer @RelaxedMockK - lateinit var gatekeeper: NetworkGatekeeper + lateinit var manager: SessionManager @RelaxedMockK lateinit var read: ByteReadChannel @@ -27,19 +27,20 @@ internal class GameServerTest { @BeforeEach fun setup() { + manager = mockk(relaxed = true) server = spyk( GameServer( - gatekeeper, + manager, 2, - mockk(relaxed = true), mockk(relaxed = true) ) ) + server.loginServer = mockk(relaxed = true) } @Test fun `Login limit exceeded`() = runTest { - every { gatekeeper.connections("") } returns 1000 + every { manager.count("") } returns 1000 server.connect(read, write, "") @@ -60,4 +61,19 @@ internal class GameServerTest { write.close() } } + + @Test + fun `No login server response`() = runTest { + every { manager.count("") } returns 1000 + coEvery { read.readByte() } returns 14 + server.loginServer = null + + server.connect(read, write, "123") + + coVerify { + manager.add("123") + write.writeByte(Response.LOGIN_SERVER_OFFLINE) + manager.remove("123") + } + } } diff --git a/network/src/test/kotlin/world/gregs/voidps/network/LoginServerTest.kt b/network/src/test/kotlin/world/gregs/voidps/network/LoginServerTest.kt index 072e51acb2..486c0a9af5 100644 --- a/network/src/test/kotlin/world/gregs/voidps/network/LoginServerTest.kt +++ b/network/src/test/kotlin/world/gregs/voidps/network/LoginServerTest.kt @@ -20,7 +20,7 @@ internal class LoginServerTest { lateinit var network: LoginServer @RelaxedMockK - lateinit var gatekeeper: NetworkGatekeeper + lateinit var manager: SessionManager @RelaxedMockK lateinit var loader: AccountLoader @@ -33,8 +33,9 @@ internal class LoginServerTest { @BeforeEach fun setup() { + manager = mockk(relaxed = true) network = spyk( - LoginServer(protocol(mockk()), 123, BigInteger.ONE, BigInteger.valueOf(2), gatekeeper, loader) + LoginServer(protocol(mockk()), 123, BigInteger.ONE, BigInteger.valueOf(2), manager, loader) ) } @@ -126,7 +127,7 @@ internal class LoginServerTest { every { rsa.readString() } returns "pass" every { packet.remaining } returns 1 every { packet.readBytes(1) } returns byteArrayOf(0) - every { gatekeeper.connected("") } returns true + every { manager.count("") } returns 1 network.validateSession(read, rsa, packet, write, "") @@ -139,8 +140,7 @@ internal class LoginServerTest { @Test fun `World full`() = runTest { val client: Client = mockk(relaxed = true) - every { client.address } returns "address" - every { gatekeeper.connect("name", "address") } returns null + every { manager.add("name") } returns null network.login(read, client, "name", "password", 1) @@ -152,8 +152,7 @@ internal class LoginServerTest { @Test fun `Read packet instructions`() = runTest { val client: Client = mockk(relaxed = true) - every { client.address } returns "address" - every { gatekeeper.connect("name", "address") } returns 123 + every { manager.add("name") } returns 123 coEvery { loader.load(client, any(), any(), any(), any()) } returns null network.login(read, client, "name", "password", 1)